Module Overview
Estimated Time: 4 hours | Difficulty: Beginner-Intermediate | Prerequisites: TypeScript basics
<View> is not a <div> pretending to be native — it is a real UIView on iOS and a real android.view.View on Android. The practical implication: your app inherits the platform’s native scrolling physics, touch feedback, font rendering, and accessibility features for free. But it also means you need to think about platform differences from the start, because a <Text> component renders differently on iOS (using Core Text) than on Android (using the Skia/HarfBuzz stack).
What You’ll Learn:
- View and SafeAreaView containers
- Text and typography
- Image optimization and caching
- TextInput and keyboard handling
- Pressable and touch interactions
- Platform-specific components
Component Mapping
React Native components compile to native platform components:| React Native | iOS (UIKit) | Android | Web Equivalent |
|---|---|---|---|
<View> | UIView | android.view.View | <div> |
<Text> | UILabel | TextView | <p>, <span> |
<Image> | UIImageView | ImageView | <img> |
<TextInput> | UITextField | EditText | <input> |
<ScrollView> | UIScrollView | ScrollView | <div> with overflow |
<FlatList> | UITableView | RecyclerView | Virtual list |
<Pressable> | UIButton | Button | <button> |
View Component
View is the most fundamental component—a container that supports layout, styling, touch handling, and accessibility.
import { View, StyleSheet } from 'react-native';
function ViewExample() {
return (
<View style={styles.container}>
<View style={styles.row}>
<View style={[styles.box, styles.red]} />
<View style={[styles.box, styles.green]} />
<View style={[styles.box, styles.blue]} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#f5f5f5',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
},
box: {
width: 80,
height: 80,
borderRadius: 8,
},
red: { backgroundColor: '#ef4444' },
green: { backgroundColor: '#22c55e' },
blue: { backgroundColor: '#3b82f6' },
});
View Props
<View
// Styling
style={styles.container}
// Accessibility
accessible={true}
accessibilityLabel="Main container"
accessibilityRole="none"
accessibilityHint="Contains the main content"
// Touch handling
onTouchStart={(e) => console.log('Touch started')}
onTouchMove={(e) => console.log('Touch moved')}
onTouchEnd={(e) => console.log('Touch ended')}
// Pointer events
pointerEvents="auto" // 'auto' | 'none' | 'box-none' | 'box-only'
// Layout
onLayout={(e) => {
const { x, y, width, height } = e.nativeEvent.layout;
console.log('Layout:', { x, y, width, height });
}}
// Testing
testID="main-container"
>
{/* Children */}
</View>
SafeAreaView
Renders content within safe area boundaries (avoiding notches, home indicators):import { SafeAreaView, View, Text, StyleSheet } from 'react-native';
// Basic SafeAreaView (iOS only)
function BasicSafeArea() {
return (
<SafeAreaView style={styles.container}>
<Text>This avoids the notch!</Text>
</SafeAreaView>
);
}
// Better: react-native-safe-area-context (cross-platform)
import { SafeAreaProvider, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
function App() {
return (
<SafeAreaProvider>
<MainScreen />
</SafeAreaProvider>
);
}
function MainScreen() {
const insets = useSafeAreaInsets();
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
<Text>Safe on all platforms!</Text>
</View>
);
}
// Or use SafeAreaView component
function AlternativeScreen() {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<Text>Safe content</Text>
</SafeAreaView>
);
}
Text Component
All text must be wrapped in a<Text> component. Unlike web development where you can put text anywhere inside a <div>, React Native will throw a runtime error if you write <View>Hello</View>. This is because native platforms have dedicated text rendering systems — UILabel on iOS and TextView on Android — and React Native needs to know which rendering pipeline to use. The strict separation may feel verbose at first, but it gives you precise control over text behavior (truncation, selection, accessibility) that would be harder to achieve with bare string nodes.
import { Text, StyleSheet, View } from 'react-native';
function TextExample() {
return (
<View style={styles.container}>
{/* Basic text */}
<Text style={styles.title}>Welcome to React Native</Text>
{/* Nested text (inherits parent styles) */}
<Text style={styles.paragraph}>
This is <Text style={styles.bold}>bold</Text> and this is{' '}
<Text style={styles.italic}>italic</Text> and this is{' '}
<Text style={styles.link} onPress={() => console.log('Link pressed')}>
a link
</Text>.
</Text>
{/* Truncated text */}
<Text numberOfLines={2} ellipsizeMode="tail" style={styles.truncated}>
This is a very long text that will be truncated after two lines.
You won't see the rest of this text because it exceeds the limit
that we have set for this particular text component.
</Text>
{/* Selectable text */}
<Text selectable style={styles.selectable}>
Long press to select this text
</Text>
{/* Adjustable font size */}
<Text
adjustsFontSizeToFit
numberOfLines={1}
style={styles.adjustable}
>
This text will shrink to fit on one line
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#1f2937',
marginBottom: 16,
},
paragraph: {
fontSize: 16,
lineHeight: 24,
color: '#4b5563',
marginBottom: 16,
},
bold: {
fontWeight: 'bold',
},
italic: {
fontStyle: 'italic',
},
link: {
color: '#3b82f6',
textDecorationLine: 'underline',
},
truncated: {
fontSize: 14,
color: '#6b7280',
marginBottom: 16,
},
selectable: {
fontSize: 14,
color: '#059669',
marginBottom: 16,
},
adjustable: {
fontSize: 24,
fontWeight: 'bold',
},
});
Text Props Reference
<Text
// Content control
numberOfLines={2}
ellipsizeMode="tail" // 'head' | 'middle' | 'tail' | 'clip'
// Selection
selectable={true}
selectionColor="#3b82f6"
// Accessibility
accessible={true}
accessibilityRole="header" // 'header' | 'link' | 'button' | etc.
accessibilityLabel="Welcome message"
// Press handling
onPress={() => console.log('Pressed')}
onLongPress={() => console.log('Long pressed')}
onPressIn={() => console.log('Press in')}
onPressOut={() => console.log('Press out')}
// Layout
onTextLayout={(e) => {
const { lines } = e.nativeEvent;
console.log('Number of lines:', lines.length);
}}
// iOS specific -- these have no effect on Android
adjustsFontSizeToFit={true} // Shrinks text to fit container (iOS only)
minimumFontScale={0.5} // Minimum scale factor when adjusting (iOS only)
allowFontScaling={true} // Respects user's accessibility font size setting
// Android specific -- these have no effect on iOS
textBreakStrategy="highQuality" // 'simple' | 'highQuality' | 'balanced'
android_hyphenationFrequency="normal" // Controls hyphenation for word wrapping
>
Text content
</Text>
iOS vs Android text rendering differences to watch for:
- Font weights: iOS supports all nine numeric weights (100-900) with the system font. Android’s Roboto supports fewer intermediate weights — requesting
fontWeight: '350'may silently snap to the nearest available weight. - Line height: iOS calculates line height from the font’s ascender and descender metrics. Android adds extra padding. The same
lineHeight: 24will produce slightly different vertical spacing on each platform. - Font scaling: Both platforms respect the user’s accessibility font size preferences by default (
allowFontScaling={true}). If your layout breaks with large text, test with accessibility font sizes enabled (iOS Settings > Display > Text Size, Android Settings > Display > Font Size). UsemaxFontSizeMultiplierto cap scaling for layout-critical text.
Image Component
Display images from local files, network URLs, or base64 data.Local Images
import { Image, StyleSheet, View } from 'react-native';
function LocalImageExample() {
return (
<View style={styles.container}>
{/* Local image (bundled with app) */}
<Image
source={require('../assets/images/logo.png')}
style={styles.logo}
/>
{/* With explicit dimensions */}
<Image
source={require('../assets/images/hero.png')}
style={{ width: 300, height: 200 }}
resizeMode="contain"
/>
</View>
);
}
Network Images
import { Image, StyleSheet, View, ActivityIndicator } from 'react-native';
import { useState } from 'react';
function NetworkImageExample() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
return (
<View style={styles.imageContainer}>
{loading && (
<ActivityIndicator
style={StyleSheet.absoluteFill}
size="large"
color="#3b82f6"
/>
)}
<Image
source={{
uri: 'https://picsum.photos/400/300',
// Optional headers for authenticated requests
headers: {
Authorization: 'Bearer token',
},
// Cache control (iOS only -- Android uses OkHttp's default cache behavior,
// which respects HTTP cache headers. There is no equivalent prop for Android.)
cache: 'default', // 'default' | 'reload' | 'force-cache' | 'only-if-cached'
}}
style={styles.networkImage}
resizeMode="cover"
onLoadStart={() => setLoading(true)}
onLoadEnd={() => setLoading(false)}
onError={(e) => {
setError(true);
setLoading(false);
console.error('Image load error:', e.nativeEvent.error);
}}
// Fallback for errors
defaultSource={require('../assets/images/placeholder.png')}
// Blur placeholder (iOS)
blurRadius={loading ? 10 : 0}
/>
{error && (
<View style={styles.errorOverlay}>
<Text>Failed to load image</Text>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
imageContainer: {
width: 300,
height: 200,
backgroundColor: '#f3f4f6',
borderRadius: 12,
overflow: 'hidden',
},
networkImage: {
width: '100%',
height: '100%',
},
errorOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
},
});
ImageBackground
import { ImageBackground, Text, StyleSheet, View } from 'react-native';
function ImageBackgroundExample() {
return (
<ImageBackground
source={require('../assets/images/background.jpg')}
style={styles.background}
resizeMode="cover"
imageStyle={styles.backgroundImage}
>
<View style={styles.overlay}>
<Text style={styles.title}>Welcome</Text>
<Text style={styles.subtitle}>Your journey starts here</Text>
</View>
</ImageBackground>
);
}
const styles = StyleSheet.create({
background: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
backgroundImage: {
opacity: 0.8,
},
overlay: {
backgroundColor: 'rgba(0,0,0,0.4)',
padding: 40,
borderRadius: 16,
alignItems: 'center',
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: '#fff',
marginBottom: 8,
},
subtitle: {
fontSize: 18,
color: '#e5e7eb',
},
});
Image Optimization with expo-image
The built-in<Image> component from React Native has no disk caching for network images on Android and limited caching control on iOS. For production apps that display many network images (product catalogs, social feeds, user avatars), this means repeated network requests, flickering on re-renders, and wasted bandwidth. The expo-image library (or alternatives like react-native-fast-image) solves this with proper memory and disk caching, blurhash placeholders, and animated transitions.
npx expo install expo-image
import { Image } from 'expo-image';
function OptimizedImage() {
return (
<Image
source="https://picsum.photos/400/300"
style={{ width: 300, height: 200 }}
contentFit="cover"
transition={200}
placeholder={require('../assets/images/placeholder.png')}
// Or use blurhash
placeholder={{ blurhash: 'LGF5]+Yk^6#M@-5c,1J5@[or[Q6.' }}
cachePolicy="memory-disk"
/>
);
}
TextInput Component
For user text input with full keyboard control. TextInput is where mobile development gets genuinely different from web: you need to manage the software keyboard explicitly. The keyboard slides up and can obscure your inputs, the return key behavior is different per keyboard type, and iOS and Android handle autofill, secure text, and autocapitalize differently. The example below demonstrates the pattern of chaining focus between inputs using refs — this is what makes a form feel native (press “Next” on the keyboard and the cursor jumps to the next field).import { TextInput, View, Text, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
import { useState, useRef } from 'react';
function TextInputExample() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [bio, setBio] = useState('');
const emailRef = useRef<TextInput>(null);
const passwordRef = useRef<TextInput>(null);
const bioRef = useRef<TextInput>(null);
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
{/* Basic text input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Name</Text>
<TextInput
style={styles.input}
value={name}
onChangeText={setName}
placeholder="Enter your name"
placeholderTextColor="#9ca3af" // Always set this explicitly -- the default
// placeholder color differs between iOS (light gray) and Android (darker gray),
// causing inconsistent appearance if you rely on defaults.
autoCapitalize="words" // Capitalizes first letter of each word
autoCorrect={false} // Disable autocorrect for names
returnKeyType="next" // Shows "Next" on the keyboard return key
onSubmitEditing={() => emailRef.current?.focus()} // Chain to next input
blurOnSubmit={false} // Keep keyboard open when pressing "Next"
/>
</View>
{/* Email input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput
ref={emailRef}
style={styles.input}
value={email}
onChangeText={setEmail}
placeholder="Enter your email"
placeholderTextColor="#9ca3af"
keyboardType="email-address" // Shows @ and . on the keyboard
autoCapitalize="none" // Emails should never auto-capitalize
autoComplete="email" // Enables Android autofill suggestions
textContentType="emailAddress" // Enables iOS autofill from Keychain
// Tip: autoComplete (Android) and textContentType (iOS) serve the same
// purpose on different platforms. Set both for full cross-platform autofill.
returnKeyType="next"
onSubmitEditing={() => passwordRef.current?.focus()}
blurOnSubmit={false}
/>
</View>
{/* Password input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Password</Text>
<TextInput
ref={passwordRef}
style={styles.input}
value={password}
onChangeText={setPassword}
placeholder="Enter your password"
placeholderTextColor="#9ca3af"
secureTextEntry
autoComplete="password"
textContentType="password"
returnKeyType="next"
onSubmitEditing={() => bioRef.current?.focus()}
blurOnSubmit={false}
/>
</View>
{/* Multiline input */}
<View style={styles.inputGroup}>
<Text style={styles.label}>Bio</Text>
<TextInput
ref={bioRef}
style={[styles.input, styles.multiline]}
value={bio}
onChangeText={setBio}
placeholder="Tell us about yourself"
placeholderTextColor="#9ca3af"
multiline
numberOfLines={4}
textAlignVertical="top"
maxLength={500}
returnKeyType="done"
/>
<Text style={styles.charCount}>{bio.length}/500</Text>
</View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#374151',
marginBottom: 8,
},
input: {
borderWidth: 1,
borderColor: '#d1d5db',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
color: '#1f2937',
},
multiline: {
height: 120,
textAlignVertical: 'top',
},
charCount: {
fontSize: 12,
color: '#9ca3af',
textAlign: 'right',
marginTop: 4,
},
});
TextInput Props Reference
<TextInput
// Value
value={text}
onChangeText={(text) => setText(text)}
defaultValue="Initial value"
// Appearance
placeholder="Enter text..."
placeholderTextColor="#999"
style={styles.input}
// Keyboard
keyboardType="default" // 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'decimal-pad' | 'url'
keyboardAppearance="light" // iOS: 'default' | 'light' | 'dark'
returnKeyType="done" // 'done' | 'go' | 'next' | 'search' | 'send'
// Behavior
autoCapitalize="sentences" // 'none' | 'sentences' | 'words' | 'characters'
autoCorrect={true}
autoComplete="email" // 'email' | 'password' | 'username' | 'name' | etc.
// textContentType is iOS-only and powers the Keychain autofill feature.
// autoComplete is the cross-platform equivalent for basic autofill hints.
// On Android, autoComplete triggers the Autofill Framework (API 26+).
textContentType="emailAddress" // iOS Keychain autofill
// Security
secureTextEntry={false}
// Multiline
multiline={false}
numberOfLines={4}
textAlignVertical="top" // Android: 'auto' | 'top' | 'bottom' | 'center'
// Limits
maxLength={100}
// Selection
selection={{ start: 0, end: 5 }}
selectionColor="#3b82f6"
// Events
onFocus={() => console.log('Focused')}
onBlur={() => console.log('Blurred')}
onSubmitEditing={() => console.log('Submitted')}
onEndEditing={() => console.log('Editing ended')}
onSelectionChange={(e) => console.log(e.nativeEvent.selection)}
// Refs
ref={inputRef}
blurOnSubmit={true}
// Accessibility
accessible={true}
accessibilityLabel="Email input"
/>
Pressable Component
Modern, flexible touch handling component (recommended over TouchableOpacity).Pressable gives you a render prop pattern where both style and children can be functions that receive a { pressed } state object. This is cleaner than TouchableOpacity because you control exactly what happens visually — you are not locked into an opacity fade. On Android, you can also add the native Material Design ripple effect via android_ripple, which iOS users never see (each platform gets its expected feedback pattern).
import { Pressable, Text, StyleSheet, View } from 'react-native';
function PressableExample() {
return (
<View style={styles.container}>
{/* Basic pressable */}
<Pressable
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
]}
onPress={() => console.log('Pressed')}
>
{({ pressed }) => (
<Text style={[styles.buttonText, pressed && styles.textPressed]}>
{pressed ? 'Pressing...' : 'Press Me'}
</Text>
)}
</Pressable>
{/* With all press events */}
<Pressable
style={styles.button}
onPress={() => console.log('onPress')}
onPressIn={() => console.log('onPressIn')}
onPressOut={() => console.log('onPressOut')}
onLongPress={() => console.log('onLongPress')}
delayLongPress={500}
hitSlop={10} // Increase touch area
pressRetentionOffset={{ top: 10, left: 10, right: 10, bottom: 10 }}
>
<Text style={styles.buttonText}>All Events</Text>
</Pressable>
{/* Android ripple effect */}
<Pressable
style={styles.button}
android_ripple={{
color: 'rgba(255, 255, 255, 0.3)',
borderless: false,
foreground: true,
}}
onPress={() => console.log('Ripple!')}
>
<Text style={styles.buttonText}>Android Ripple</Text>
</Pressable>
{/* Disabled state */}
<Pressable
style={({ pressed }) => [
styles.button,
styles.buttonDisabled,
]}
disabled={true}
onPress={() => console.log('Will not fire')}
>
<Text style={[styles.buttonText, styles.textDisabled]}>
Disabled
</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
padding: 20,
gap: 16,
},
button: {
backgroundColor: '#3b82f6',
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
buttonPressed: {
backgroundColor: '#2563eb',
transform: [{ scale: 0.98 }],
},
buttonDisabled: {
backgroundColor: '#9ca3af',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
textPressed: {
opacity: 0.8,
},
textDisabled: {
opacity: 0.6,
},
});
Custom Button Component
import { Pressable, Text, StyleSheet, ActivityIndicator, ViewStyle, TextStyle } from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
icon?: React.ReactNode;
style?: ViewStyle;
textStyle?: TextStyle;
}
export function Button({
title,
onPress,
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
icon,
style,
textStyle,
}: ButtonProps) {
const isDisabled = disabled || loading;
return (
<Pressable
style={({ pressed }) => [
styles.base,
styles[variant],
styles[size],
pressed && !isDisabled && styles.pressed,
isDisabled && styles.disabled,
style,
]}
onPress={onPress}
disabled={isDisabled}
>
{loading ? (
<ActivityIndicator
color={variant === 'outline' || variant === 'ghost' ? '#3b82f6' : '#fff'}
size="small"
/>
) : (
<>
{icon}
<Text
style={[
styles.text,
styles[`${variant}Text`],
styles[`${size}Text`],
isDisabled && styles.disabledText,
textStyle,
]}
>
{title}
</Text>
</>
)}
</Pressable>
);
}
const styles = StyleSheet.create({
base: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 8,
gap: 8,
},
// Variants
primary: {
backgroundColor: '#3b82f6',
},
secondary: {
backgroundColor: '#6b7280',
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: '#3b82f6',
},
ghost: {
backgroundColor: 'transparent',
},
// Sizes
sm: {
paddingVertical: 8,
paddingHorizontal: 16,
},
md: {
paddingVertical: 12,
paddingHorizontal: 20,
},
lg: {
paddingVertical: 16,
paddingHorizontal: 24,
},
// States
pressed: {
opacity: 0.8,
transform: [{ scale: 0.98 }],
},
disabled: {
opacity: 0.5,
},
// Text
text: {
fontWeight: '600',
},
primaryText: {
color: '#fff',
},
secondaryText: {
color: '#fff',
},
outlineText: {
color: '#3b82f6',
},
ghostText: {
color: '#3b82f6',
},
smText: {
fontSize: 14,
},
mdText: {
fontSize: 16,
},
lgText: {
fontSize: 18,
},
disabledText: {
opacity: 0.7,
},
});
ScrollView Component
For scrollable content:import { ScrollView, View, Text, StyleSheet, RefreshControl } from 'react-native';
import { useState, useCallback } from 'react';
function ScrollViewExample() {
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(() => {
setRefreshing(true);
// Simulate API call
setTimeout(() => setRefreshing(false), 2000);
}, []);
return (
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor="#3b82f6"
colors={['#3b82f6']} // Android
/>
}
// Scroll behavior
bounces={true} // iOS bounce effect
overScrollMode="always" // Android: 'auto' | 'always' | 'never'
scrollEventThrottle={16} // For smooth scroll tracking
onScroll={(e) => {
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
const isCloseToBottom =
layoutMeasurement.height + contentOffset.y >= contentSize.height - 20;
if (isCloseToBottom) {
console.log('Near bottom!');
}
}}
// Keyboard behavior -- these two props are critical for forms inside ScrollViews:
// "handled" means taps on interactive elements (buttons) work even when the
// keyboard is open, but tapping empty space still dismisses the keyboard.
// The alternative "always" prevents keyboard dismissal on any tap, and "never"
// (the default) blocks taps on buttons while the keyboard is visible -- a
// common source of "my button doesn't work" bug reports.
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag" // Dismiss keyboard when user starts scrolling
>
{Array.from({ length: 20 }).map((_, i) => (
<View key={i} style={styles.item}>
<Text style={styles.itemText}>Item {i + 1}</Text>
</View>
))}
</ScrollView>
);
}
// Horizontal ScrollView
function HorizontalScrollExample() {
return (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.horizontalContent}
pagingEnabled={false} // Set true for paging behavior
snapToInterval={160} // Snap to card width
decelerationRate="fast"
>
{Array.from({ length: 10 }).map((_, i) => (
<View key={i} style={styles.card}>
<Text style={styles.cardText}>Card {i + 1}</Text>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
scrollView: {
flex: 1,
},
contentContainer: {
padding: 20,
},
item: {
backgroundColor: '#fff',
padding: 20,
marginBottom: 12,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
itemText: {
fontSize: 16,
color: '#1f2937',
},
horizontalContent: {
paddingHorizontal: 20,
paddingVertical: 10,
},
card: {
width: 150,
height: 100,
backgroundColor: '#3b82f6',
borderRadius: 12,
marginRight: 12,
justifyContent: 'center',
alignItems: 'center',
},
cardText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});
Platform-Specific Code
import { Platform, StyleSheet, View, Text } from 'react-native';
function PlatformExample() {
return (
<View style={styles.container}>
<Text style={styles.text}>
Running on {Platform.OS} {Platform.Version}
</Text>
{/* Platform-specific rendering */}
{Platform.OS === 'ios' && <Text>iOS only content</Text>}
{Platform.OS === 'android' && <Text>Android only content</Text>}
{/* Platform.select */}
<Text style={styles.platformText}>
{Platform.select({
ios: 'Hello iOS!',
android: 'Hello Android!',
default: 'Hello!',
})}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
},
text: {
fontSize: 16,
marginBottom: 10,
},
platformText: {
fontSize: 18,
fontWeight: 'bold',
...Platform.select({
ios: {
color: '#007AFF',
},
android: {
color: '#3DDC84',
},
}),
},
});
// Platform-specific files
// Button.ios.tsx - iOS implementation
// Button.android.tsx - Android implementation
// Button.tsx - Default/shared implementation
Hands-On Exercise: Profile Card
Build a complete profile card component:import { View, Text, Image, Pressable, StyleSheet } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
interface ProfileCardProps {
name: string;
title: string;
avatar: string;
followers: number;
following: number;
posts: number;
isFollowing: boolean;
onFollowPress: () => void;
onMessagePress: () => void;
}
export function ProfileCard({
name,
title,
avatar,
followers,
following,
posts,
isFollowing,
onFollowPress,
onMessagePress,
}: ProfileCardProps) {
return (
<View style={styles.card}>
<Image source={{ uri: avatar }} style={styles.avatar} />
<Text style={styles.name}>{name}</Text>
<Text style={styles.title}>{title}</Text>
<View style={styles.stats}>
<View style={styles.stat}>
<Text style={styles.statValue}>{posts}</Text>
<Text style={styles.statLabel}>Posts</Text>
</View>
<View style={styles.stat}>
<Text style={styles.statValue}>{followers}</Text>
<Text style={styles.statLabel}>Followers</Text>
</View>
<View style={styles.stat}>
<Text style={styles.statValue}>{following}</Text>
<Text style={styles.statLabel}>Following</Text>
</View>
</View>
<View style={styles.actions}>
<Pressable
style={({ pressed }) => [
styles.button,
isFollowing ? styles.followingButton : styles.followButton,
pressed && styles.buttonPressed,
]}
onPress={onFollowPress}
>
<Text style={[
styles.buttonText,
isFollowing && styles.followingButtonText,
]}>
{isFollowing ? 'Following' : 'Follow'}
</Text>
</Pressable>
<Pressable
style={({ pressed }) => [
styles.button,
styles.messageButton,
pressed && styles.buttonPressed,
]}
onPress={onMessagePress}
>
<Ionicons name="chatbubble-outline" size={18} color="#3b82f6" />
<Text style={styles.messageButtonText}>Message</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 24,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 5,
},
avatar: {
width: 100,
height: 100,
borderRadius: 50,
marginBottom: 16,
},
name: {
fontSize: 22,
fontWeight: 'bold',
color: '#1f2937',
marginBottom: 4,
},
title: {
fontSize: 14,
color: '#6b7280',
marginBottom: 20,
},
stats: {
flexDirection: 'row',
marginBottom: 24,
},
stat: {
alignItems: 'center',
paddingHorizontal: 24,
},
statValue: {
fontSize: 20,
fontWeight: 'bold',
color: '#1f2937',
},
statLabel: {
fontSize: 12,
color: '#9ca3af',
marginTop: 4,
},
actions: {
flexDirection: 'row',
gap: 12,
},
button: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
gap: 6,
},
buttonPressed: {
opacity: 0.8,
transform: [{ scale: 0.98 }],
},
followButton: {
backgroundColor: '#3b82f6',
},
followingButton: {
backgroundColor: '#f3f4f6',
borderWidth: 1,
borderColor: '#e5e7eb',
},
messageButton: {
backgroundColor: '#eff6ff',
borderWidth: 1,
borderColor: '#bfdbfe',
},
buttonText: {
fontSize: 14,
fontWeight: '600',
color: '#fff',
},
followingButtonText: {
color: '#374151',
},
messageButtonText: {
fontSize: 14,
fontWeight: '600',
color: '#3b82f6',
},
});
Core Component Pitfalls
Mistakes that bite mobile developers at every level:Forgetting
KeyboardAvoidingView behavior differs per platform. On iOS, use behavior="padding" — it adds padding below the content equal to the keyboard height. On Android, use behavior="height" or omit the prop entirely — Android’s windowSoftInputMode in AndroidManifest.xml often handles keyboard avoidance automatically (the default is adjustResize). Using "padding" on Android can cause double-adjustment, pushing your content too far up.Using ScrollView for dynamic lists. If your list data comes from an API and could be 50 or 500 items, always use FlatList. Even if your initial data is small, a ScrollView renders everything upfront, which blocks the JS thread during the initial render. On a budget Android device with 3 GB RAM, rendering 200 items in a ScrollView can trigger an out-of-memory kill.Not setting placeholderTextColor on TextInput. On iOS, the default placeholder color is a light gray that works on white backgrounds. On Android, the default can vary by device manufacturer and theme — some Samsung and Xiaomi devices render nearly invisible placeholders. Always set placeholderTextColor explicitly.Ignoring hitSlop on small touch targets. Apple’s HIG recommends a minimum 44x44pt touch target. Android’s Material Design recommends 48x48dp. If your icon button is 24x24, users will miss taps and blame your app. Use hitSlop to expand the touchable area without changing the visual size: hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}.Using the built-in <Image> for image-heavy screens. The default <Image> component does not cache network images to disk on Android. On screens showing 20+ network images (product grids, photo feeds), this means re-downloading images every time the component remounts. Switch to expo-image or react-native-fast-image for proper memory and disk caching.Next Steps
Module 6: Styling & Theming
Learn advanced styling patterns, theming, and dark mode implementation