Expo SDK
Overview
Expo SDK 54+ provides a managed React Native development environment with file-based routing (Expo Router), native module access, and streamlined build tooling. This skill covers app configuration, the root layout provider pattern, and key Expo/RN libraries.
Prerequisite: npx create-expo-app or Expo SDK 54+ in package.json
Workflows
Setting up a new Expo demo:
-
Create project: npx create-expo-app [demo-name] --template blank-typescript
-
Install core dependencies: pnpm add expo-router expo-image expo-haptics react-native-reanimated react-native-gesture-handler react-native-safe-area-context @gorhom/bottom-sheet @shopify/flash-list lucide-react-native nativewind tailwindcss@3
-
Configure NativeWind (see nativewind skill)
-
Set up root layout with provider stack
-
Configure app.json with scheme, name, splash
-
Add route groups and screens
-
Run: pnpm start (Expo dev server)
Adding a new library:
-
Install with pnpm: pnpm add [library]
-
Check if Expo config plugin needed in app.json
-
Rebuild dev client if native module added: npx expo prebuild
Guidance
app.json Configuration
Key fields for demo apps:
Field Purpose
expo.name
Display name
expo.slug
URL-safe identifier
expo.scheme
Deep link scheme (e.g., myapp )
expo.orientation
portrait (default for demos)
expo.splash
Splash screen configuration
expo.ios.bundleIdentifier
iOS bundle ID
expo.android.package
Android package name
expo.plugins
Expo config plugins (e.g., expo-router )
Root Layout Provider Pattern
The root app/_layout.tsx wraps the entire app with providers. Standard order:
GestureHandlerRootView (flex: 1) └── SafeAreaProvider └── ThemeProvider / Context └── Stack (Expo Router)
-
GestureHandlerRootView must be outermost (required by gesture handler and bottom sheets)
-
SafeAreaProvider provides safe area insets to all descendants
-
App-level context providers go between SafeAreaProvider and Stack
-
<Stack screenOptions={{ headerShown: false }} /> for custom headers
expo-image (replaces RN Image)
Use expo-image for all image rendering — provides caching, blurhash placeholders, content-fit modes, and animated transitions.
Key props:
-
source — URI string or require() for local images
-
placeholder — blurhash string for loading state
-
contentFit — 'cover' | 'contain' | 'fill'
-
transition — fade-in duration in ms (e.g., 300 )
expo-haptics
Provide tactile feedback on interactions:
-
Haptics.selectionAsync() — light tap for selections, toggles
-
Haptics.impactAsync(ImpactFeedbackStyle.Medium) — button press, card tap
-
Haptics.notificationAsync(NotificationFeedbackType.Success) — action completion
Use sparingly — haptics on every touch is annoying.
Safe Area Insets
Account for device notch, status bar, and home indicator:
-
useSafeAreaInsets() — returns { top, bottom, left, right } in points
-
Apply to screen containers: paddingTop: insets.top
-
NativeWind classes: use pt-[${insets.top}px] or wrap in SafeAreaView
@gorhom/bottom-sheet
Replaces Radix Dialog for mobile modal patterns:
-
Use for detail views, selections, filters, forms
-
Define snap points: snapPoints={['25%', '50%', '90%']}
-
Backdrop: backdropComponent with press-to-dismiss
-
BottomSheetScrollView for scrollable content inside sheets
-
Requires GestureHandlerRootView as ancestor
FlashList (replaces FlatList)
High-performance list rendering from @shopify/flash-list :
-
Drop-in FlatList replacement with mandatory estimatedItemSize prop
-
estimatedItemSize={80} — estimated height of each item in points
-
Recycling architecture for smooth 60fps scrolling
-
Use contentContainerClassName for NativeWind styling
lucide-react-native
Icon library for React Native (matches web lucide-react):
-
Import individual icons: import { Home, Settings, ChevronRight } from 'lucide-react-native'
-
Props: size , color , strokeWidth
-
Consistent icon set across mobile and web codebases
StatusBar
Configure status bar appearance per screen:
-
<StatusBar style="dark" /> for light backgrounds
-
<StatusBar style="light" /> for dark backgrounds
-
Import from expo-status-bar
Best Practices
-
Wrap root layout in GestureHandlerRootView with style={{ flex: 1 }}
-
Use expo-image for all images (caching, blurhash, performance)
-
Add haptics to primary actions only (buttons, major selections) — not every touch
-
Set estimatedItemSize on all FlashList components
-
Place providers in root _layout.tsx , not in individual screens
-
Use useSafeAreaInsets() for manual padding, SafeAreaView for simple wrapping
-
Test on real device for haptics and performance verification
Anti-Patterns
-
Using React Native Image instead of expo-image
-
Using FlatList for large datasets instead of FlashList
-
Forgetting GestureHandlerRootView (causes bottom sheet and gesture crashes)
-
Overusing haptics on every interaction
-
Hardcoding status bar height instead of using safe area insets
-
Missing estimatedItemSize on FlashList (required prop, console warning)
-
Placing SafeAreaView inside ScrollView (causes layout issues)
-
Not including expo-router plugin in app.json plugins array