Skip to main content

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.

Core Components

Module Overview

Estimated Time: 4 hours | Difficulty: Beginner-Intermediate | Prerequisites: TypeScript basics
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 NativeiOS (UIKit)AndroidWeb Equivalent
<View>UIViewandroid.view.View<div>
<Text>UILabelTextView<p>, <span>
<Image>UIImageViewImageView<img>
<TextInput>UITextFieldEditText<input>
<ScrollView>UIScrollViewScrollView<div> with overflow
<FlatList>UITableViewRecyclerViewVirtual list
<Pressable>UIButtonButton<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: 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.

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