React Native lets you build real iOS and Android apps in JavaScript with ~95% shared code between platforms. It powers apps at Meta, Microsoft, Shopify, and thousands of startups. This roadmap shows exactly what to learn, in what order, and what to build along the way.
At a glance
| Phase |
Topic |
Timeline |
| 0 |
Prerequisites: JavaScript + React |
Weeks 1–4 |
| 1 |
React Native core |
Weeks 5–7 |
| 2 |
Styling & layouts |
Weeks 8–9 |
| 3 |
Navigation |
Weeks 10–11 |
| 4 |
State management |
Weeks 12–14 |
| 5 |
Networking & APIs |
Weeks 15–16 |
| 6 |
Device features & native modules |
Weeks 17–19 |
| 7 |
Testing |
Weeks 20–22 |
| 8 |
Performance |
Weeks 23–24 |
| 9 |
CI/CD & publishing |
Weeks 25–28 |
| 10 |
Advanced topics |
Weeks 29–36 |
Phase 0 — Prerequisites: JavaScript + React (Weeks 1–4)
React Native is JavaScript + React. Skip this if you already know both.
JavaScript you must know
// Arrow functions, destructuring, spread
const greet = ({ name, age = 25 }) => `Hello ${name}, ${age}`;
// Array methods
const doubled = [1, 2, 3].map(n => n * 2);
const evens = [1, 2, 3, 4].filter(n => n % 2 === 0);
// Promises and async/await
async function fetchUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Modules
import { useState } from 'react';
export default function App() { /* ... */ }
React concepts you must know
| Concept |
Why it matters in RN |
| Functional components |
Everything in RN is functional |
useState |
Local UI state |
useEffect |
Side effects, data fetching |
useRef |
Refs to native views |
| Props + children |
Component composition |
| Custom hooks |
Reusable logic across screens |
| Context API |
Light global state (auth, theme) |
Phase 1 — React Native core (Weeks 5–7)
Setting up your environment
Expo (recommended for beginners)
npx create-expo-app MyApp
cd MyApp
npx expo start
React Native CLI (for advanced native work)
npx @react-native-community/cli@latest init MyApp
cd MyApp
npx react-native run-android # or run-ios
| Approach |
Pros |
Cons |
| Expo managed |
Fast setup, OTA updates, no Xcode/AS needed |
Limited native APIs |
| Expo bare |
Expo DX + full native access |
Larger project |
| React Native CLI |
Full control |
Complex setup, slow start |
Core components
import {
View, Text, TextInput, Button, TouchableOpacity,
FlatList, ScrollView, Image, StyleSheet, Platform
} from 'react-native';
export default function HomeScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Hello, React Native</Text>
<TouchableOpacity style={styles.btn} onPress={() => alert('Tap!')}>
<Text style={styles.btnText}>Press me</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
title: { fontSize: 24, fontWeight: 'bold' },
btn: { backgroundColor: '#6366f1', padding: 12, borderRadius: 8, marginTop: 16 },
btnText: { color: '#fff', fontWeight: '600' },
});
Core component cheat sheet
| Component |
Web equivalent |
Notes |
View |
div |
Basic layout container |
Text |
p, span |
ALL text must be in <Text> |
TextInput |
input |
onChangeText, value |
Image |
img |
source={{ uri }} or require() |
ScrollView |
div with overflow |
Renders all children at once |
FlatList |
virtualized list |
Use for long lists — lazy render |
SectionList |
grouped list |
Like FlatList with section headers |
TouchableOpacity |
button |
Fades on press |
Pressable |
button |
More control over press states |
Modal |
modal/dialog |
Full-screen overlay |
ActivityIndicator |
spinner |
Loading state |
Switch |
checkbox/toggle |
Boolean toggle |
Phase 2 — Styling & layouts (Weeks 8–9)
React Native uses a subset of CSS properties via StyleSheet. No real CSS — no classes, no cascade.
Flexbox in React Native
Flexbox is the only layout system. Default flexDirection is column (not row like web).
// Horizontal centering
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text>Left</Text>
<Text>Right</Text>
</View>
// Fill remaining space
<View style={{ flex: 1 }}> {/* takes all remaining space */}
<Text>Fills the screen</Text>
</View>
Platform-specific styles
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
shadow: Platform.select({
ios: {
shadowColor: '#000',
shadowOpacity: 0.15,
shadowRadius: 8,
shadowOffset: { width: 0, height: 2 },
},
android: {
elevation: 4,
},
}),
});
Responsive design
import { Dimensions, useWindowDimensions } from 'react-native';
// Static (doesn't update on orientation change)
const { width, height } = Dimensions.get('window');
// Dynamic (updates on rotate — preferred)
function MyComponent() {
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
return (
<View style={{ flexDirection: isLandscape ? 'row' : 'column' }}>
{/* ... */}
</View>
);
}
Phase 3 — Navigation (Weeks 10–11)
React Navigation is the de facto standard.
npm install @react-navigation/native
npx expo install react-native-screens react-native-safe-area-context
# Stack navigator
npm install @react-navigation/native-stack
# Tab navigator
npm install @react-navigation/bottom-tabs
# Drawer navigator
npm install @react-navigation/drawer react-native-gesture-handler react-native-reanimated
Stack navigator
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} options={{ title: 'Item Details' }} />
</Stack.Navigator>
</NavigationContainer>
);
}
// Navigate
function HomeScreen({ navigation }) {
return (
<TouchableOpacity onPress={() => navigation.navigate('Details', { id: 42 })}>
<Text>Go to details</Text>
</TouchableOpacity>
);
}
// Receive params
function DetailsScreen({ route }) {
const { id } = route.params;
return <Text>Item {id}</Text>;
}
Bottom tab navigator
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator screenOptions={({ route }) => ({
tabBarIcon: ({ color, size }) => {
const icons = { Home: 'home', Profile: 'person', Settings: 'settings' };
return <Ionicons name={icons[route.name]} size={size} color={color} />;
},
})}>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
);
}
Navigation patterns
| Pattern |
Navigator |
Use case |
| Stack |
native-stack |
Screen-to-screen flow (push/pop) |
| Tab |
bottom-tabs |
Top-level app sections |
| Drawer |
drawer |
Side menu navigation |
| Modal |
Stack with presentation: 'modal' |
Full-screen overlays |
| Nested |
Any combination |
Tab with nested stacks |
Phase 4 — State management (Weeks 12–14)
When to use what
| State type |
Solution |
| Local UI state |
useState |
| Shared simple state |
Context + useReducer |
| Complex global state |
Zustand / Redux Toolkit |
| Server state + caching |
React Query / SWR |
| Form state |
React Hook Form |
Zustand (recommended for most apps)
npm install zustand
import { create } from 'zustand';
interface CartStore {
items: { id: number; qty: number }[];
add: (id: number) => void;
remove: (id: number) => void;
total: () => number;
}
const useCart = create<CartStore>((set, get) => ({
items: [],
add: (id) => set(state => ({
items: state.items.find(i => i.id === id)
? state.items.map(i => i.id === id ? { ...i, qty: i.qty + 1 } : i)
: [...state.items, { id, qty: 1 }],
})),
remove: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })),
total: () => get().items.reduce((sum, i) => sum + i.qty, 0),
}));
// In a component
function CartIcon() {
const total = useCart(s => s.total());
return <Text>{total} items</Text>;
}
Redux Toolkit (enterprise / large teams)
npm install @reduxjs/toolkit react-redux
import { createSlice, configureStore } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => { state.value += 1; },
decrement: state => { state.value -= 1; },
},
});
export const store = configureStore({ reducer: { counter: counterSlice.reducer } });
Phase 5 — Networking & APIs (Weeks 15–16)
Fetch with error handling
async function apiFetch<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(url, {
...options,
headers: { 'Content-Type': 'application/json', ...options?.headers },
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
return res.json();
}
React Query (best practice for server state)
npm install @tanstack/react-query
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
);
}
function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => apiFetch<User[]>('https://api.example.com/users'),
});
if (isLoading) return <ActivityIndicator />;
if (error) return <Text>Error: {error.message}</Text>;
return (
<FlatList
data={data}
keyExtractor={u => String(u.id)}
renderItem={({ item }) => <Text>{item.name}</Text>}
/>
);
}
Offline storage
| Package |
Use case |
@react-native-async-storage/async-storage |
Key-value, small data |
expo-sqlite / react-native-sqlite-storage |
Structured relational data |
react-native-mmkv |
Fast key-value (10× AsyncStorage) |
WatermelonDB |
Large offline-first databases |
import AsyncStorage from '@react-native-async-storage/async-storage';
await AsyncStorage.setItem('token', jwt);
const token = await AsyncStorage.getItem('token');
await AsyncStorage.removeItem('token');
Phase 6 — Device features & native modules (Weeks 17–19)
Expo modules (zero native config)
| Feature |
Package |
| Camera |
expo-camera |
| Location |
expo-location |
| Push notifications |
expo-notifications |
| File system |
expo-file-system |
| Image picker |
expo-image-picker |
| Secure storage |
expo-secure-store |
| Haptics |
expo-haptics |
| Sensors |
expo-sensors |
| Biometrics |
expo-local-authentication |
| Contacts |
expo-contacts |
Location example
import * as Location from 'expo-location';
async function getCurrentLocation() {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
alert('Location permission denied');
return;
}
const loc = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High });
return { lat: loc.coords.latitude, lng: loc.coords.longitude };
}
Push notifications with Expo
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
async function registerForPushNotifications() {
if (!Device.isDevice) return null; // must run on real device
const { status: existingStatus } = await Notifications.getPermissionsAsync();
const finalStatus = existingStatus === 'granted'
? existingStatus
: (await Notifications.requestPermissionsAsync()).status;
if (finalStatus !== 'granted') return null;
const token = (await Notifications.getExpoPushTokenAsync()).data;
return token; // send this to your backend
}
Phase 7 — Testing (Weeks 20–22)
Testing pyramid for React Native
| Level |
Tool |
What to test |
| Unit |
Jest |
Pure functions, hooks, store logic |
| Component |
React Native Testing Library |
UI rendering, interactions |
| E2E |
Detox / Maestro |
Full user flows on real device/emulator |
Jest + React Native Testing Library
npm install --save-dev @testing-library/react-native jest-expo
import { render, screen, fireEvent } from '@testing-library/react-native';
import Counter from './Counter';
test('increments count on press', () => {
render(<Counter />);
const btn = screen.getByText('+');
fireEvent.press(btn);
expect(screen.getByText('1')).toBeTruthy();
});
test('shows loading state', async () => {
render(<UserProfile userId={1} />);
expect(screen.getByTestId('spinner')).toBeTruthy();
await screen.findByText('John Doe');
});
Mocking native modules
// __mocks__/@react-native-async-storage/async-storage.ts
const store: Record<string, string> = {};
export default {
getItem: jest.fn(key => Promise.resolve(store[key] ?? null)),
setItem: jest.fn((key, val) => { store[key] = val; return Promise.resolve(); }),
removeItem: jest.fn(key => { delete store[key]; return Promise.resolve(); }),
};
Phase 8 — Performance (Weeks 23–24)
Common performance pitfalls
| Problem |
Symptom |
Fix |
| Anonymous functions in render |
Unnecessary re-renders |
useCallback |
| Expensive calculations in render |
Slow frames |
useMemo |
ScrollView for long lists |
High memory, slow scroll |
FlatList with keyExtractor |
Missing key on list items |
Console warning, wrong diffs |
Add stable key prop |
| Large images |
Slow load, memory |
Resize + cache with expo-image |
| JS thread blocked |
Dropped frames, no gesture response |
Move heavy work to workers / Reanimated |
| Hermes disabled |
Slower JS startup |
Enable Hermes (default in RN 0.70+) |
FlatList optimisation
<FlatList
data={items}
keyExtractor={item => item.id.toString()}
renderItem={renderItem} // memoize with useCallback
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews={true}
getItemLayout={(_, index) => ({ // only if item height is fixed
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
/>
Animations with Reanimated
npm install react-native-reanimated
import Animated, { useSharedValue, withSpring, useAnimatedStyle } from 'react-native-reanimated';
function SpringBox() {
const offset = useSharedValue(0);
const style = useAnimatedStyle(() => ({
transform: [{ translateY: offset.value }],
}));
return (
<>
<Animated.View style={[{ width: 60, height: 60, backgroundColor: '#6366f1' }, style]} />
<Button title="Bounce" onPress={() => { offset.value = withSpring(-100); }} />
</>
);
}
Phase 9 — CI/CD & publishing (Weeks 25–28)
EAS Build (Expo Application Services)
npm install -g eas-cli
eas login
eas build:configure
# Build for both platforms
eas build --platform all --profile preview
# Submit to stores
eas submit --platform ios
eas submit --platform android
EAS profiles in eas.json
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"android": { "buildType": "apk" }
},
"production": {
"autoIncrement": true
}
}
}
GitHub Actions for Expo
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:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- run: eas build --platform all --non-interactive
App store requirements
| Step |
iOS |
Android |
| Developer account |
Apple Developer ($99/yr) |
Google Play ($25 one-time) |
| Binary format |
.ipa |
.aab (preferred) / .apk |
| Review time |
1–3 days |
Hours–1 day |
| OTA updates |
Expo Updates (JS only) |
Expo Updates (JS only) |
| Native code updates |
Full store submission |
Full store submission |
Phase 10 — Advanced topics (Weeks 29–36)
New Architecture (Fabric + JSI)
React Native 0.71+ ships with the new architecture enabled by default. Key improvements:
| Old architecture |
New architecture |
| Async bridge |
JSI — synchronous JS ↔ native calls |
| JSON serialisation overhead |
Direct memory access |
| Async layout |
Synchronous layout with Fabric |
UIManager.dispatchViewManagerCommand |
Direct calls via HostComponent |
TypeScript best practices
// Type your navigation params
type RootStackParamList = {
Home: undefined;
Details: { id: number; title: string };
Profile: { userId: string };
};
// Type-safe navigation prop
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
type HomeNavProp = NativeStackNavigationProp<RootStackParamList, 'Home'>;
function HomeScreen({ navigation }: { navigation: HomeNavProp }) {
navigation.navigate('Details', { id: 1, title: 'Test' }); // fully typed
}
Monorepo with Expo + web
npx create-expo-app --template tabs
# Add Next.js for web in a Turborepo / pnpm workspace
Custom native module (Expo Modules API)
// modules/my-module/src/index.ts
import MyModule from './MyModuleModule';
export function getDeviceName(): string {
return MyModule.getDeviceName();
}
// iOS: MyModuleModule.swift
// Android: MyModuleModule.kt
// Both implement getDeviceName() returning a String
Full technology map
Language → JavaScript / TypeScript
Runtime → React Native (Hermes engine)
UI → Core components, Expo UI, NativeWind (Tailwind)
Navigation → React Navigation, Expo Router
State → Zustand, Redux Toolkit, Context API
Server state → TanStack Query, SWR
Forms → React Hook Form + Zod
HTTP → Fetch, Axios
Storage → AsyncStorage, MMKV, SQLite, WatermelonDB
Auth → Clerk, Firebase Auth, Supabase Auth
Push → Expo Notifications, Firebase FCM
Animations → Reanimated, Moti, Lottie
Testing → Jest, RNTL, Detox, Maestro
Build → Expo EAS, Fastlane
CI/CD → GitHub Actions, Bitrise, CircleCI
Maps → react-native-maps, MapLibre
36-week timeline
| Weeks |
Milestone |
| 1–4 |
JavaScript ES6+ + React hooks solid |
| 5–7 |
First RN app running on device, core components |
| 8–9 |
Styled layouts that look good on iOS + Android |
| 10–11 |
Multi-screen app with tabs + stack |
| 12–14 |
State management with Zustand |
| 15–16 |
API data displayed in FlatList with loading/error states |
| 17–19 |
Camera, location, or push notifications working |
| 20–22 |
60%+ test coverage on key components |
| 23–24 |
Smooth animations, no JS thread jank |
| 25–28 |
App live on TestFlight / Play internal testing |
| 29–36 |
Production app shipped, TypeScript strict, monorepo |
Portfolio project ideas
| Project |
What you practice |
| Todo app with AsyncStorage |
State, storage, CRUD |
| Weather app |
Geolocation, external API, FlatList |
| Chat app (Firebase) |
Real-time listeners, auth, Firestore |
| E-commerce product list |
Navigation, React Query, Cart with Zustand |
| Fitness tracker |
Device sensors, charts, SQLite |
| Social feed with camera |
Camera, image upload, pagination |
React Native developer roles & salary
| Role |
Experience |
US salary |
| Junior RN Developer |
0–2 years |
$60k–$90k |
| Mid RN Developer |
2–4 years |
$90k–$130k |
| Senior RN Developer |
4+ years |
$130k–$170k |
| Mobile Tech Lead |
6+ years |
$160k–$200k |
| Staff Mobile Engineer |
8+ years |
$190k–$240k |
React Native developers who also know Expo, TypeScript, and CI/CD (EAS) typically command a 10–20% premium over developers who know vanilla RN only.
Common mistakes
| Mistake |
Problem |
Fix |
Using ScrollView for long lists |
Memory leak on 1000+ items |
FlatList with keyExtractor |
| Ignoring TypeScript |
Runtime crashes from bad types |
Enable strict: true in tsconfig |
| Storing secrets in app bundle |
Secrets exposed by reverse-engineering |
Backend proxy, never in client |
| Not testing on real devices |
iOS/Android differences missed |
Test on both platforms early |
| Deep nesting without navigation |
Hard to debug, back button broken |
Use React Navigation from day 1 |
| Re-renders every keystroke |
Laggy text input |
Controlled inputs + useCallback |
| Blocking JS thread |
Dropped frames, unresponsive UI |
Reanimated worklets, batch state updates |
Not memoizing list renderItem |
Full re-render on every state change |
useCallback + React.memo |
React Native vs Flutter vs Kotlin Multiplatform
| Dimension |
React Native |
Flutter |
KMP |
| Language |
JavaScript / TypeScript |
Dart |
Kotlin |
| UI rendering |
Native OS widgets |
Custom Canvas (Skia) |
Native OS widgets per platform |
| Shared code |
~95% |
~98% |
Logic only (UI per platform) |
| Performance |
Near-native |
Near-native |
Native |
| Learning curve |
Low (if you know React) |
Medium |
High |
| Ecosystem |
Huge (npm) |
Growing |
Small |
| Hot reload |
Yes |
Yes |
Partial |
| Web support |
React Native Web |
Yes |
No |
| Job market 2025 |
Large |
Large |
Small but growing |
| Companies |
Meta, Microsoft, Shopify |
Google, BMW, Alibaba |
JetBrains, large enterprises |
Frequently asked questions
Should I start with Expo or React Native CLI?
Start with Expo. It removes 80% of setup complexity, supports all common native APIs via Expo modules, and lets you publish with EAS Build without Xcode or Android Studio for most use cases. Switch to bare workflow when you need a custom native module that Expo doesn't support.
Do I need to know iOS/Android development?
Not to start. You can build and ship a production app with only JavaScript. But as you advance — debugging native crashes, writing custom modules, configuring provisioning profiles — knowledge of Swift/Objective-C (iOS) or Kotlin/Java (Android) becomes valuable.
Is React Native dead / dying?
No. Meta actively maintains it, and the New Architecture (Fabric + JSI) shipped in 0.71+, making it significantly faster. Shopify, Microsoft, and hundreds of Fortune 500 companies use it in production.
React Native vs Flutter — which should I learn?
If you already know React/JavaScript, React Native is faster to learn and shares a huge npm ecosystem. If you're starting fresh and want best-in-class UI performance and a unified rendering engine, Flutter is excellent. Both are solid choices with strong job markets.
Can React Native build for web?
Yes, via react-native-web. Some teams maintain a single codebase for iOS, Android, and web. Expo also has first-class web support. However, the web story is secondary — if web is your primary target, use Next.js or Remix instead.
What's the best way to handle auth?
Clerk (@clerk/clerk-expo) or Supabase Auth are the fastest paths to production-ready auth with social login, MFA, and session management. For full control, implement JWT + refresh token rotation yourself with Axios interceptors and MMKV storage.