react-native-expert
Expert-level React Native development covering Expo vs bare workflow decisions, navigation architecture with React Navigation, performance optimization with JSI/TurboModules, state management, native modules, EAS Build CI/CD, testing strategies, and platform-specific patterns. Trigger phrases: React
React Native Expert
React Native lets you build genuinely native iOS and Android apps with React, sharing most of your codebase across platforms. The key to doing it well is understanding the architecture: the JavaScript thread communicates with the native thread, and that bridge is where performance lives or dies. The New Architecture (JSI, Fabric, TurboModules) dramatically improves this, but you need to understand the old to appreciate why the new matters.
Core Mental Model
React Native has three threads:
- JS Thread: Your React code, business logic, state management
- UI Thread (Main): Native rendering, user input, animations
- Native Modules Thread: Platform APIs (camera, filesystem, etc.)
Communication between these threads used to go through a JSON-serialized bridge — the performance bottleneck. The New Architecture (enabled by default in RN 0.74+) replaces this with JSI (JavaScript Interface) — a C++ layer allowing JS to directly call native functions synchronously.
Rule: Anything touching the UI thread from the JS thread is potentially janky. Animations, gesture responses, and high-frequency updates need to run on the native thread via Reanimated, Gesture Handler, or InteractionManager.
Expo vs Bare React Native
Managed Workflow (Expo Go + EAS)
Use when: Your native dependencies are available in Expo's SDK, you want fast iteration, you don't need custom native modules.Pros:
✅ Fastest setup (expo create-app → running in 5 minutes)
✅ Over-the-air updates via expo-updates (ship JS changes without App Store review)
✅ EAS Build handles Xcode/Gradle complexity in the cloud
✅ expo-dev-client gives you a custom dev build with your dependencies
✅ Expo SDK covers 90%+ of common needs (camera, location, notifications, etc.)
Cons:
❌ Cannot add arbitrary native (Kotlin/Swift/Objective-C) code
❌ SDK update cadence controls your RN version
❌ Debugging native issues harder without bare access
Bare Workflow
Use when: You need custom native modules, full control over the native project, or third-party native SDKs (Maps, Stripe Native, custom BLE, etc.).# Start bare from scratch
npx react-native@latest init MyApp --template react-native-template-typescript
# Or eject from managed Expo
npx expo eject # one-way operation — can't go back easily
# You can still use Expo modules in bare!
npx expo install expo-camera expo-location
# Then run: npx expo prebuild --platform ios
expo-dev-client (Best of Both)
Run your own native build that looks like Expo Go — enables managed workflow features while supporting custom native modules.npx expo install expo-dev-client
# In app.json: { "expo": { "plugins": ["expo-dev-client"] } }
npx expo run:ios # builds and installs the dev client
Navigation with React Navigation
Hierarchy Architecture
// Root navigator — defines the app's navigation structure
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
const Stack = createNativeStackNavigator<RootStackParams>()
const Tab = createBottomTabNavigator<TabParams>()
// Type-safe navigation params
type RootStackParams = {
Auth: undefined
Main: undefined
Modal: { itemId: string }
}
type TabParams = {
Home: undefined
Profile: undefined
Search: undefined
}
function TabNavigator() {
return (
<Tab.Navigator
screenOptions={{
tabBarActiveTintColor: '#2dd4bf',
tabBarStyle: { backgroundColor: '#0a0a0a' },
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
)
}
function RootNavigator() {
const { isAuthenticated } = useAuth()
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<>
<Stack.Screen name="Main" component={TabNavigator} />
<Stack.Screen
name="Modal"
component={ModalScreen}
options={{ presentation: 'modal', headerShown: true }}
/>
</>
) : (
<Stack.Screen name="Auth" component={AuthScreen} />
)}
</Stack.Navigator>
)
}
export default function App() {
return (
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
)
}
Deep Linking Setup
// Linking configuration
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Main: {
screens: {
Home: 'home',
Profile: 'profile/:userId',
},
},
Modal: 'items/:itemId',
},
},
}
<NavigationContainer linking={linking}>
<!-- iOS: ios/MyApp/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
<!-- iOS Universal Links: apple-app-site-association file on your server -->
<!-- Android: android/app/src/main/AndroidManifest.xml -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
<data android:scheme="https" android:host="myapp.com" />
</intent-filter>
Navigation State Persistence
import AsyncStorage from '@react-native-async-storage/async-storage'
const PERSISTENCE_KEY = 'NAVIGATION_STATE'
export default function App() {
const [initialState, setInitialState] = React.useState()
const [isReady, setIsReady] = React.useState(false)
React.useEffect(() => {
const restoreState = async () => {
const savedStateString = await AsyncStorage.getItem(PERSISTENCE_KEY)
const state = savedStateString ? JSON.parse(savedStateString) : undefined
if (state !== undefined) setInitialState(state)
setIsReady(true)
}
restoreState()
}, [])
if (!isReady) return null
return (
<NavigationContainer
initialState={initialState}
onStateChange={(state) =>
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state))
}
>
<RootNavigator />
</NavigationContainer>
)
}
Performance
FlatList Optimization
type Item = { id: string; title: string; imageUrl: string }
function OptimizedList({ items }: { items: Item[] }) {
// getItemLayout: enables scrolling to index without measuring all items
const getItemLayout = useCallback(
(_: any, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}),
[],
)
// keyExtractor: must be stable and unique — don't use index
const keyExtractor = useCallback((item: Item) => item.id, [])
// renderItem: memoize to prevent unnecessary re-renders
const renderItem = useCallback(
({ item }: { item: Item }) => <ListItem item={item} />,
[],
)
return (
<FlatList
data={items}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
// Performance props
maxToRenderPerBatch={10} // Items rendered per batch (default: 10)
windowSize={5} // Render window = 5 × screen height
initialNumToRender={10} // Initial render count
removeClippedSubviews={true} // Unmount off-screen views (be careful with complex views)
// Pagination
onEndReached={onLoadMore}
onEndReachedThreshold={0.5} // Trigger when 50% from bottom
// Pull-to-refresh
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={onRefresh} />
}
/>
)
}
// Always memoize list items to prevent re-renders
const ListItem = React.memo(({ item }: { item: Item }) => {
return (
<View style={styles.item}>
<FastImage source={{ uri: item.imageUrl }} style={styles.image} />
<Text>{item.title}</Text>
</View>
)
})
InteractionManager for Deferred Work
import { InteractionManager } from 'react-native'
// Defer expensive setup until after navigation animation completes
function ExpensiveScreen() {
const [isReady, setIsReady] = React.useState(false)
React.useEffect(() => {
const task = InteractionManager.runAfterInteractions(() => {
// Runs after animations/interactions are complete
setIsReady(true)
})
return () => task.cancel()
}, [])
if (!isReady) return <SkeletonLoader />
return <ExpensiveContent />
}
Reanimated for Smooth Animations
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
runOnJS,
} from 'react-native-reanimated'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
function DraggableCard() {
const translateY = useSharedValue(0)
const opacity = useSharedValue(1)
const gesture = Gesture.Pan()
.onUpdate((event) => {
translateY.value = event.translationY // Runs on UI thread — no bridge
})
.onEnd(() => {
if (Math.abs(translateY.value) > 150) {
// Dismiss card
translateY.value = withTiming(translateY.value > 0 ? 500 : -500)
opacity.value = withTiming(0, {}, () => {
runOnJS(onDismiss)() // Call JS function from UI thread
})
} else {
translateY.value = withSpring(0)
}
})
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: translateY.value }],
opacity: opacity.value,
}))
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[styles.card, animatedStyle]} />
</GestureDetector>
)
}
State Management
Zustand (recommended for RN)
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import AsyncStorage from '@react-native-async-storage/async-storage'
type AuthState = {
token: string | null
user: User | null
isLoading: boolean
login: (email: string, password: string) => Promise<void>
logout: () => void
}
const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
user: null,
isLoading: false,
login: async (email, password) => {
set({ isLoading: true })
try {
const { token, user } = await api.login(email, password)
set({ token, user, isLoading: false })
} catch (error) {
set({ isLoading: false })
throw error
}
},
logout: () => set({ token: null, user: null }),
}),
{
name: 'auth-storage',
storage: createJSONStorage(() => AsyncStorage),
},
),
)
Native Module Integration
// Creating a custom native module
// 1. iOS: Create MyModule.swift + expose via Objective-C bridge
// 2. Android: Create MyModule.kt + register in MyPackage.kt
// 3. JS: NativeModules.MyModule or createNativeWrapper
import { NativeModules, NativeEventEmitter, Platform } from 'react-native'
const { BiometricModule } = NativeModules
// Call native method
async function authenticate(): Promise<boolean> {
if (Platform.OS === 'ios') {
return BiometricModule.authenticateWithFaceID()
} else {
return BiometricModule.authenticateWithFingerprint()
}
}
// Subscribe to native events
const emitter = new NativeEventEmitter(BiometricModule)
const subscription = emitter.addListener('onAuthResult', (result) => {
console.log('Auth result:', result)
})
// Cleanup in useEffect return
EAS Build for CI/CD
// eas.json
{
"cli": { "version": ">= 5.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": false },
"env": { "APP_ENV": "development" }
},
"preview": {
"distribution": "internal",
"channel": "preview",
"env": { "APP_ENV": "staging" }
},
"production": {
"distribution": "store",
"channel": "production",
"env": { "APP_ENV": "production" },
"ios": { "autoIncrement": true },
"android": { "autoIncrement": true }
}
},
"submit": {
"production": {
"ios": {
"appleId": "[email protected]",
"ascAppId": "1234567890",
"appleTeamId": "ABCDE12345"
},
"android": {
"serviceAccountKeyPath": "./google-services.json",
"track": "internal"
}
}
}
}
# .github/workflows/eas-build.yml
name: EAS Build
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- run: eas build --platform all --profile production --non-interactive
Over-the-Air Updates
// app.json — configure update channel
{
"expo": {
"updates": {
"url": "https://u.expo.dev/your-project-id",
"fallbackToCacheTimeout": 0,
"checkAutomatically": "ON_LOAD"
},
"runtimeVersion": {
"policy": "sdkVersion" // or "nativeVersion" for more control
}
}
}
// Manual update check in app
import * as Updates from 'expo-updates'
async function checkForUpdate() {
try {
const update = await Updates.checkForUpdateAsync()
if (update.isAvailable) {
await Updates.fetchUpdateAsync()
await Updates.reloadAsync()
}
} catch (error) {
// Handle error — network offline, etc.
}
}
Testing
Jest + React Native Testing Library
// Component test
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native'
import { LoginScreen } from './LoginScreen'
describe('LoginScreen', () => {
it('submits form with email and password', async () => {
const mockLogin = jest.fn().mockResolvedValue({ token: 'abc' })
render(<LoginScreen onLogin={mockLogin} />)
fireEvent.changeText(screen.getByLabelText('Email'), '[email protected]')
fireEvent.changeText(screen.getByLabelText('Password'), 'password123')
fireEvent.press(screen.getByText('Sign In'))
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith('[email protected]', 'password123')
})
})
it('shows error when login fails', async () => {
const mockLogin = jest.fn().mockRejectedValue(new Error('Invalid credentials'))
render(<LoginScreen onLogin={mockLogin} />)
fireEvent.press(screen.getByText('Sign In'))
await waitFor(() => {
expect(screen.getByText('Invalid credentials')).toBeTruthy()
})
})
})
Detox E2E Testing
// e2e/login.test.js
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp()
})
beforeEach(async () => {
await device.reloadReactNative()
})
it('logs in with valid credentials', async () => {
await element(by.id('email-input')).typeText('[email protected]')
await element(by.id('password-input')).typeText('password123')
await element(by.id('login-button')).tap()
await expect(element(by.text('Home'))).toBeVisible()
})
})
Platform-Specific Code
import { Platform, StyleSheet } from 'react-native'
// Inline platform selection
const paddingTop = Platform.OS === 'ios' ? 44 : 24
// Platform.select
const styles = StyleSheet.create({
header: {
paddingTop: Platform.select({
ios: 44,
android: 24,
}),
...Platform.select({
ios: { shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 4 },
android: { elevation: 4 },
}),
},
})
// File extension platform splitting:
Button.ios.tsx → loaded on iOS
Button.android.tsx → loaded on Android
Button.tsx → fallback for both (or web)
// Usage in code: import Button from './Button'
// RN bundler automatically resolves platform extension
Anti-Patterns
❌ setState in fast-updating callbacks — Calling setState on every scroll event or gesture update is a bridge flood. Use useRef for frequently updating values that don't need to trigger re-renders.
❌ Anonymous renderItem functions — renderItem={({ item }) => <Component />} creates a new function every render, breaking memoization. Extract and memoize.
❌ Missing keyExtractor on FlatList — Using array index as key breaks list virtualization and animations when items reorder.
❌ Heavy computation on render — Sorting, filtering, transforming data inside render runs on every re-render. useMemo for derived data.
❌ Not separating transactional vs. styled navigation — Using gesture navigation for modals that should be interruption-free (payment, auth flows). Use presentation: 'modal' appropriately.
❌ console.log in production — Logging is expensive. Strip it in production builds or use conditional logging.
Quick Reference
FlatList Performance Checklist
- [ ]
keyExtractorreturns stable unique ID (not index) - [ ]
renderItemis memoized (useCallback+React.memoon component) - [ ]
getItemLayoutdefined for fixed-height rows - [ ]
maxToRenderPerBatchandwindowSizetuned for content type - [ ]
removeClippedSubviews={true}for long lists
EAS Build Commands
eas build --platform ios --profile development
eas build --platform android --profile preview
eas build --platform all --profile production --non-interactive
eas submit --platform ios --profile production
eas update --channel production --message "Hot fix: crash on launch"
Platform Gotchas
| Feature | iOS | Android |
| Safe area insets | SafeAreaView from react-native-safe-area-context | SafeAreaView same |
| Shadow | shadowColor/Opacity/Radius/Offset | elevation: N |
| Text fonts | System font = San Francisco | System font = Roboto |
| Back button | Swipe gesture | Hardware back button → handle in nav |
| Status bar | StatusBar component | StatusBar + translucent |
| Keyboard | KeyboardAvoidingView behavior="padding" | behavior="height" |
Skill Information
- Source
- MoltbotDen
- Category
- Coding Agents & IDEs
- Repository
- View on GitHub
Related Skills
go-expert
Write idiomatic, production-quality Go code. Use when building Go APIs, CLIs, microservices, or systems code. Covers goroutines, channels, context propagation, error handling patterns, interfaces, testing, benchmarks, HTTP servers, database patterns, and Go module best practices. Expert-level Go idioms that senior engineers expect.
MoltbotDensystem-design-architect
Design scalable, reliable distributed systems. Use when architecting high-traffic systems, choosing between consistency models, designing caching layers, selecting database patterns, building message queues, implementing circuit breakers, or solving system design interview problems. Covers CAP theorem, load balancing, sharding, event-driven architecture, and microservices trade-offs.
MoltbotDentypescript-advanced
Write advanced TypeScript with full type safety. Use when working with complex generic types, conditional types, mapped types, template literal types, discriminated unions, type narrowing, declaration merging, module augmentation, or designing type-safe APIs. Covers TypeScript 5.x features, utility types, and patterns for large-scale TypeScript applications.
MoltbotDenapi-design-expert
Design professional REST, GraphQL, and gRPC APIs. Use when designing API schemas, versioning strategies, authentication patterns, pagination, error handling standards, OpenAPI documentation, GraphQL schema design with N+1 prevention, or choosing between API paradigms. Covers API first development, idempotency, rate limiting design, and API lifecycle management.
MoltbotDenrust-systems
Write safe, performant Rust systems code. Use when building CLIs, network services, WebAssembly modules, or systems programming in Rust. Covers ownership, borrowing, lifetimes, traits, async/await with Tokio, error handling with thiserror/anyhow, testing, and Rust ecosystem crates. Idiomatic Rust patterns that pass code review.
MoltbotDen