> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 05. Core Components Deep Dive

> Master View, Text, Image, TextInput, Pressable, and all essential React Native components

<Frame>
  <img src="https://mintcdn.com/devweeekends/X0Fp4X8lMl-ZftoO/images/courses/react-native-crash-course/core-components.svg?fit=max&auto=format&n=X0Fp4X8lMl-ZftoO&q=85&s=29ffb419a37f44b4417bcfe068833faa" alt="Core Components" width="800" height="500" data-path="images/courses/react-native-crash-course/core-components.svg" />
</Frame>

## Module Overview

<Info>
  **Estimated Time**: 4 hours | **Difficulty**: Beginner-Intermediate | **Prerequisites**: TypeScript basics
</Info>

React Native provides a set of core components that map directly to native platform views. Unlike web development where you use HTML elements, React Native uses components that render to actual native UI elements.

This means your `<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.

```tsx theme={null}
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

```tsx theme={null}
<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):

```tsx theme={null}
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.

```tsx theme={null}
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

```tsx theme={null}
<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>
```

<Note>
  **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: 24` will 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). Use `maxFontSizeMultiplier` to cap scaling for layout-critical text.
</Note>

***

## Image Component

Display images from local files, network URLs, or base64 data.

### Local Images

```tsx theme={null}
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

```tsx theme={null}
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

```tsx theme={null}
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.

```bash theme={null}
npx expo install expo-image
```

```tsx theme={null}
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).

```tsx theme={null}
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

```tsx theme={null}
<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).

```tsx theme={null}
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

```tsx theme={null}
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:

```tsx theme={null}
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

```tsx theme={null}
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:

```tsx theme={null}
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

<Warning>
  **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.
</Warning>

***

## Next Steps

<Card title="Module 6: Styling & Theming" icon="arrow-right" href="/courses/react-native-crash-course/06-styling-theming">
  Learn advanced styling patterns, theming, and dark mode implementation
</Card>
