Published: March 04, 2026 | Cluster: coding-tools | Reading time: ~35 min
Cross-platform mobile development has never been more accessible than it is right now – demand is high enough that Udemy discounted its top-rated “React Native – The Practical Guide [2025]” course from $174.99 to $29.99 in August 2025, and Class Central’s July 2026 survey of the top React Native courses found that 8 of the 9 leading options are free, with the sole paid pick priced at just $25. This react native tutorial walks you through building a fully functional Task Manager app from a blank terminal to a published listing on both the Apple App Store and Google Play – all in one guide, at no cost. Whether you are a web developer making your first foray into mobile or an experienced engineer evaluating frameworks for a 2026 project, this tutorial gives you the foundational knowledge and the production-ready patterns you need.
React Native 0.87.1, which GitHub lists as released on August 26, 2026, is the latest stable patch of the 0.87 line that the official React Native docs now list as current, having overtaken 0.86 as the latest release in September 2026. The 0.87 release itself made the Strict TypeScript API the default, added experimental Swift Package Manager support, bumped the required Android Gradle Plugin to version 9, and brought Metro updates alongside a fresh round of DevTools improvements, per Meta’s release notes. That cadence has been remarkably steady: the 0.86 line, which had already shipped Android 15 edge-to-edge support, cut its release branch on June 9, 2026, and its latest patch, 0.86.3, was still marked actively supported by the endoflife.date tracker in August 2026; before that, 0.85 landed in April 2026 with the New Architecture finally treated as stable and went on to pick up its 0.85.3 patch in May 2026 per endoflife.date, 0.84.1 followed on February 27, 2026 (after the 0.84 line released February 11, 2026), and 0.83 shipped in December 2025 carrying React 19.2 and new DevTools features before reaching its own 0.83.10 patch in June 2026. React itself has since moved on to 19.3.0, which launched September 9, 2026 and is now the release most relevant to current React Native tutorials. Further back, the 0.80.x line – which bundled React 19.1.0 – cut its release branch on May 7, 2025 and shipped on June 12, 2025, setting the pace for the quarterly releases that followed. The New Architecture – powered by JSI, Fabric, and TurboModules – became the default back in October 2024 with the 0.76 release, and the legacy bridge was permanently removed in 0.82. The result is a framework that feels genuinely competitive with native development in terms of performance and developer experience. If you checked React Native a couple of years ago and moved on, the landscape has changed dramatically.
This guide pairs naturally with our TypeScript tutorial – all code examples here are written in TypeScript – and our Flutter vs React Native 2026 comparison if you are still evaluating which framework fits your team. We also reference our AI coding tools guide throughout, because in 2026 a smart developer workflow leans on AI assistance at every layer of the stack.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What You Will Build: The Task Manager App
By the end of this react native tutorial, you will have a fully working Task Manager application with the following features:
- Create, edit, and delete tasks with titles, descriptions, and due dates
- Mark tasks complete with animated feedback
- Filter tasks by status (All, Active, Completed)
- Persist data locally using AsyncStorage
- Sync with a REST API backend
- Tab and stack navigation between screens
- Full TypeScript type safety throughout
- A Jest and React Native Testing Library test suite
- A production build deployed via Expo Application Services (EAS)
The app is intentionally practical. Task management touches enough real-world patterns – CRUD operations, local persistence, API calls, navigation, state management, and animations – that everything you learn here transfers directly to professional projects.
Prerequisites and Environment Requirements
Before diving into react native app development, confirm that your machine meets the minimum requirements. The 2026 toolchain has moved forward on several fronts, particularly around the Node.js minimum version and Android API level targets – floors that trace back to the 0.76 release in October 2024, when Meta’s React Native team raised the minimum iOS deployment target to 15.1 and bumped the Android minimum SDK from 23 to 24 (Android 7). As of September 2026 the framework has moved well past that baseline – React Native 0.87.1, released August 26, 2026, is the latest stable patch, with the 0.87 line requiring Node.js 22 and Android Gradle Plugin 9 while making the Strict TypeScript API the default and adding experimental Swift Package Manager support, per Meta’s release notes (Android 15 edge-to-edge support had already arrived with 0.86), so double-check your target SDK, Gradle plugin version, and safe-area handling if you are tracking the newest tooling rather than the 0.84 baseline this tutorial builds against.
| Tool / Platform | Minimum Version | Recommended Version | Notes |
|---|---|---|---|
| Node.js | 22.0.0 | 22.14.0 LTS | Required since RN 0.83 |
| React Native | 0.84.0 | 0.84.1 | Latest stable (Feb 2026) |
| Expo SDK | 55.0.0 | 55.x | Full 0.84 support |
| TypeScript | 5.4 | 5.8 | Bundled with Expo template |
| Xcode | 16.0 | 16.2 | macOS only, iOS builds |
| Android Studio | Ladybug (2024.2) | Meerkat (2024.3) | Android 16 / API 36 |
| JDK | 17 | 21 | Required for Gradle 8.x |
| Expo CLI | 0.22.0 | Latest | Install globally via npm |
You should be comfortable with JavaScript and ideally have read our TypeScript tutorial before starting, because all examples use TypeScript generics, interfaces, and strict mode. Basic React knowledge (components, props, hooks) is assumed but not expert-level familiarity.
Required Knowledge
You need working knowledge of JavaScript ES2022+ syntax including async/await, destructuring, and optional chaining. Familiarity with React functional components and the useState hook is essential. You do not need prior mobile development experience – the entire tutorial runs through Expo, which abstracts away the native build configuration until the deployment step.
Accounts You Will Need
For deployment you will need a free Expo account for EAS Build, an Apple Developer account ($99/year) for App Store distribution, and a Google Play Developer account ($25 one-time) for Play Store distribution. You can complete everything up to the deployment section without any of these accounts. If you would rather pair this free guide with a paid, instructor-led course, pricing varies widely: ACTE lists its React Native training at ₹16,500 as of January 2026, with batches running August 24–30, 2026, while SkillCertified’s online cohort is discounted 25% to $1,499 (from $1,999) for its August 14, 2026 start, and its classroom option runs $2,499 (17% off $2,999) for the August 14 and September 4, 2026 batches. Cromacampus prices its own program at ₹27,000 after a 10% discount, for batches running August 29–31, 2026.
Step 1: Setting Up Your Development Environment
A clean environment setup prevents hours of debugging cryptic errors later. Follow these steps precisely, particularly around the Node.js version – React Native 0.84 will refuse to build with Node 20 or below, and as of the 0.87 release in August 2026 the framework’s own minimum climbed again to Node.js 22, so there is no reason to install anything older.
Installing Node.js 22
Use a version manager to handle Node installations. nvm remains the most portable option across macOS and Linux, while fnm is significantly faster and is the recommended choice for 2026 workflows.
# Install fnm (Fast Node Manager)
curl -fsSL https://fnm.vercel.app/install | bash
# Reload your shell, then install and use Node 22
fnm install 22
fnm use 22
fnm default 22
# Verify
node --version # v22.14.0
npm --version # 10.9.2
Installing Expo CLI and EAS CLI
# Install Expo CLI globally
npm install -g expo-cli@latest
# Install EAS CLI for builds and submissions
npm install -g eas-cli@latest
# Verify installations
expo --version # 0.22.x or later
eas --version # 14.x or later
On macOS, also install Watchman, which React Native uses for fast file watching during development:
brew install watchman
watchman --version # 2024.12.09.00 or later
For Android development, download Android Studio Meerkat and ensure the Android 16 (API 36) SDK platform is installed via the SDK Manager. As of the May 2026 Agilesoftlabs upgrade tutorial, projects on the 0.76+ line need Android Gradle Plugin 8.3 or later, so double-check your android/build.gradle before syncing. Set the ANDROID_HOME environment variable to your SDK path, typically ~/Library/Android/sdk on macOS or C:UsersYourNameAppDataLocalAndroidSdk on Windows.
Configuring Your Editor
VS Code with the React Native Tools extension, the ESLint extension, and the Prettier extension gives you the best development experience. The React Native Tools extension provides inline debugger attachment, log streaming from connected devices, and launch configurations for both iOS and Android. Pair these with an AI coding assistant – our AI coding tools guide covers the leading options and their specific React Native capabilities in detail.
Step 2: Creating Your First React Native Project
This react native expo tutorial uses the managed Expo workflow, which is the right starting point for the vast majority of projects. The managed workflow handles native build configuration automatically, letting you focus on the JavaScript layer. You can always eject to a bare workflow if you need custom native modules later.
Initializing the Project
# Create a new Expo project with the TypeScript template
npx create-expo-app@latest TaskManager --template expo-template-blank-typescript
cd TaskManager
# Install core dependencies
npx expo install expo-router@4 @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs react-native-screens react-native-safe-area-context
# Install AsyncStorage and other utilities
npx expo install @react-native-async-storage/async-storage expo-status-bar expo-font
After initialization, your project structure will look like this:
TaskManager/
├── app/ # Expo Router file-based routing
│ ├── (tabs)/
│ │ ├── _layout.tsx # Tab navigator layout
│ │ ├── index.tsx # Tasks list screen
│ │ └── settings.tsx # Settings screen
│ ├── task/
│ │ ├── [id].tsx # Task detail/edit screen
│ │ └── new.tsx # New task screen
│ └── _layout.tsx # Root layout
├── components/ # Reusable UI components
│ ├── TaskCard.tsx
│ ├── FilterBar.tsx
│ └── TaskForm.tsx
├── hooks/ # Custom hooks
│ ├── useTasks.ts
│ └── useTheme.ts
├── services/ # API and storage layer
│ ├── api.ts
│ └── storage.ts
├── types/ # TypeScript type definitions
│ └── task.ts
├── app.json
├── tsconfig.json
└── package.json
Starting the Development Server
npx expo start
# Output:
# Starting Metro Bundler
# ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
# Metro waiting on exp://192.168.1.10:8081
# › Press a │ open Android
# › Press i │ open iOS simulator
# › Press w │ open web
Scan the QR code with the Expo Go app on your physical device or press i for iOS simulator / a for Android emulator. Expo Go supports React Native 0.84 via Expo SDK 55. Hot reloading is enabled by default – changes to your JavaScript files reflect instantly in the running app without a full restart.
Understanding the Expo Router File Convention
Expo Router uses the file system to define your navigation structure, similar to how Next.js handles web routing. Files in the app/ directory become screens, folders with _layout.tsx files define navigator wrappers, and files wrapped in parentheses like (tabs)/ create groups without affecting the URL. Dynamic route segments use brackets: task/[id].tsx matches /task/abc123 and makes abc123 available via useLocalSearchParams().
Step 3: Understanding React Native Core Components
React Native maps JavaScript components to native UI elements – there is no DOM, no HTML, and no CSS files. Understanding this mapping is the mental model shift that makes everything else click when learning react native app development. The following table shows the most important component mappings.
| React Native Component | iOS Equivalent | Android Equivalent | Web HTML Equivalent |
|---|---|---|---|
| View | UIView | android.view.View | <div> |
| Text | UILabel | TextView | <p>, <span> |
| TextInput | UITextField | EditText | <input> |
| ScrollView | UIScrollView | ScrollView | <div> with overflow |
| FlatList | UITableView | RecyclerView | <ul> (virtualized) |
| TouchableOpacity | UIButton (opacity) | Button (opacity anim) | <button> |
| Pressable | UIButton (gesture) | Button (ripple) | <button> |
| Modal | UIViewController | Dialog / AlertDialog | <dialog> |
Let’s build the foundational type definitions and the first component. Start by defining the Task type in types/task.ts:
// types/task.ts
export type TaskStatus = 'active' | 'completed';
export type TaskPriority = 'low' | 'medium' | 'high';
export interface Task {
id: string;
title: string;
description: string;
status: TaskStatus;
priority: TaskPriority;
dueDate: string | null; // ISO 8601 date string
createdAt: string;
updatedAt: string;
}
export interface TaskFilter {
status: 'all' | TaskStatus;
priority: TaskPriority | 'all';
}
Building the TaskCard Component
The TaskCard component is the workhorse of the UI. It uses Pressable (the modern replacement for TouchableOpacity), the Text component for all text rendering, and View for layout. Notice that all event handlers, styles, and types are explicitly typed – this is the standard pattern in professional React Native development. If you need a refresher on TypeScript generics and interface syntax, our TypeScript tutorial covers everything used here.
// components/TaskCard.tsx
import React, { useCallback } from 'react';
import {
View,
Text,
Pressable,
StyleSheet,
Animated,
} from 'react-native';
import { Task } from '../types/task';
interface TaskCardProps {
task: Task;
onPress: (task: Task) => void;
onToggleComplete: (id: string) => void;
}
const PRIORITY_COLORS: Record<Task['priority'], string> = {
low: '#22c55e',
medium: '#f59e0b',
high: '#ef4444',
};
export const TaskCard: React.FC<TaskCardProps> = ({
task,
onPress,
onToggleComplete,
}) => {
const scaleAnim = React.useRef(new Animated.Value(1)).current;
const handleToggle = useCallback(() => {
Animated.sequence([
Animated.timing(scaleAnim, {
toValue: 0.95,
duration: 100,
useNativeDriver: true,
}),
Animated.timing(scaleAnim, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}),
]).start();
onToggleComplete(task.id);
}, [task.id, onToggleComplete, scaleAnim]);
const isOverdue =
task.dueDate !== null &&
task.status === 'active' &&
new Date(task.dueDate) < new Date();
return (
<Animated.View style={[styles.container, { transform: [{ scale: scaleAnim }] }]}>
<Pressable
style={({ pressed }) => [
styles.card,
task.status === 'completed' && styles.completedCard,
pressed && styles.pressedCard,
]}
onPress={() => onPress(task)}
accessibilityLabel={`Task: ${task.title}`}
accessibilityRole="button"
>
<View style={styles.leftSection}>
<Pressable
style={[
styles.checkbox,
task.status === 'completed' && styles.checkboxCompleted,
]}
onPress={handleToggle}
accessibilityLabel={`Mark ${task.title} as ${task.status === 'completed' ? 'active' : 'complete'}`}
accessibilityRole="checkbox"
>
{task.status === 'completed' && (
<Text style={styles.checkmark}>✓</Text>
)}
</Pressable>
</View>
<View style={styles.content}>
<Text
style={[
styles.title,
task.status === 'completed' && styles.completedTitle,
]}
numberOfLines={2}
>
{task.title}
</Text>
{task.description.length > 0 && (
<Text style={styles.description} numberOfLines={1}>
{task.description}
</Text>
)}
<View style={styles.meta}>
<View
style={[
styles.priorityBadge,
{ backgroundColor: PRIORITY_COLORS[task.priority] },
]}
>
<Text style={styles.priorityText}>{task.priority}</Text>
</View>
{task.dueDate && (
<Text style={[styles.dueDate, isOverdue && styles.overdueDate]}>
{isOverdue ? '⚠ ' : ''}
{new Date(task.dueDate).toLocaleDateString()}
</Text>
)}
</View>
</View>
</Pressable>
</Animated.View>
);
};
const styles = StyleSheet.create({
container: { marginHorizontal: 16, marginVertical: 6 },
card: {
flexDirection: 'row',
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 3,
},
completedCard: { opacity: 0.7 },
pressedCard: { backgroundColor: '#f9fafb' },
leftSection: { justifyContent: 'flex-start', paddingRight: 12 },
checkbox: {
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
borderColor: '#d1d5db',
alignItems: 'center',
justifyContent: 'center',
},
checkboxCompleted: { backgroundColor: '#22c55e', borderColor: '#22c55e' },
checkmark: { color: '#ffffff', fontSize: 14, fontWeight: '700' },
content: { flex: 1 },
title: { fontSize: 16, fontWeight: '600', color: '#111827', marginBottom: 4 },
completedTitle: { textDecorationLine: 'line-through', color: '#9ca3af' },
description: { fontSize: 14, color: '#6b7280', marginBottom: 8 },
meta: { flexDirection: 'row', alignItems: 'center', gap: 8 },
priorityBadge: {
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 99,
},
priorityText: { fontSize: 12, fontWeight: '600', color: '#ffffff', textTransform: 'capitalize' },
dueDate: { fontSize: 12, color: '#6b7280' },
overdueDate: { color: '#ef4444', fontWeight: '600' },
});
Accessibility in React Native Components
Accessibility is not optional in production apps – both Apple App Store and Google Play review guidelines flag apps that do not meet basic accessibility standards. Every interactive element must have an accessibilityLabel, interactive controls need an accessibilityRole, and state changes should be communicated via accessibilityState. Screen readers (VoiceOver on iOS, TalkBack on Android) use these props to describe the UI to users. The TaskCard above demonstrates the pattern: every Pressable has a descriptive label and an explicit role.
Step 4: Building the UI with StyleSheet and Flexbox
React Native’s layout engine is Yoga, a cross-platform implementation of the CSS Flexbox specification. If you know CSS Flexbox, you already know 80% of React Native layout. The key differences are that flexDirection defaults to 'column' (not 'row'), all dimensions are density-independent pixels by default, and there is no cascade – styles do not inherit from parent to child except for text styles within nested Text components.
StyleSheet vs Inline Styles
Always use StyleSheet.create() rather than inline style objects. StyleSheet validates your styles in development and sends style IDs (integers) across the communication layer rather than entire objects, which reduces serialization overhead. StyleSheet also enables better TypeScript autocomplete and catches invalid properties at build time. Reserve inline styles only for values that are computed at runtime, such as dynamic colors or positions derived from state.
Building the FilterBar Component
// components/FilterBar.tsx
import React from 'react';
import { View, Text, Pressable, StyleSheet, ScrollView } from 'react-native';
import { TaskFilter } from '../types/task';
interface FilterBarProps {
filter: TaskFilter;
onFilterChange: (filter: Partial<TaskFilter>) => void;
taskCounts: { all: number; active: number; completed: number };
}
type StatusOption = { label: string; value: TaskFilter['status']; count: number };
export const FilterBar: React.FC<FilterBarProps> = ({
filter,
onFilterChange,
taskCounts,
}) => {
const statusOptions: StatusOption[] = [
{ label: 'All', value: 'all', count: taskCounts.all },
{ label: 'Active', value: 'active', count: taskCounts.active },
{ label: 'Completed', value: 'completed', count: taskCounts.completed },
];
return (
<View style={styles.container}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.scroll}
>
{statusOptions.map((option) => (
<Pressable
key={option.value}
style={[
styles.chip,
filter.status === option.value && styles.chipActive,
]}
onPress={() => onFilterChange({ status: option.value })}
accessibilityRole="radio"
accessibilityState={{ checked: filter.status === option.value }}
>
<Text
style={[
styles.chipLabel,
filter.status === option.value && styles.chipLabelActive,
]}
>
{option.label}
</Text>
<View style={styles.badge}>
<Text style={styles.badgeText}>{option.count}</Text>
</View>
</Pressable>
))}
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#f9fafb',
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
},
scroll: { paddingHorizontal: 16, paddingVertical: 12, gap: 8 },
chip: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 99,
backgroundColor: '#ffffff',
borderWidth: 1,
borderColor: '#e5e7eb',
gap: 6,
},
chipActive: { backgroundColor: '#6366f1', borderColor: '#6366f1' },
chipLabel: { fontSize: 14, fontWeight: '500', color: '#374151' },
chipLabelActive: { color: '#ffffff' },
badge: {
backgroundColor: '#e5e7eb',
borderRadius: 99,
minWidth: 20,
paddingHorizontal: 6,
paddingVertical: 2,
alignItems: 'center',
},
badgeText: { fontSize: 11, fontWeight: '700', color: '#374151' },
});
Platform-Specific Styling
Some design elements need to differ between iOS and Android to feel native. Use Platform.OS for conditional values or Platform.select() for object-based selection. A common example is shadows – iOS uses the shadow* properties while Android uses elevation. The card styles in the TaskCard component demonstrate this: both shadowColor/shadowOffset/shadowOpacity/shadowRadius and elevation are set so the shadow renders correctly on both platforms.
Step 5: Managing State with React Hooks
State management in build mobile app react native projects has converged on React’s built-in hooks for most use cases. The combination of useState, useEffect, useReducer, and custom hooks handles 95% of real-world requirements without reaching for an external library. For the Task Manager, we use useReducer for task state – since actions like add, update, delete, and toggle are well-modeled as discrete reducer actions – and useState for UI state like filters and loading indicators.
Implementing the Task Reducer
// hooks/useTasks.ts
import { useReducer, useEffect, useCallback } from 'react';
import { Task, TaskFilter } from '../types/task';
import { loadTasks, saveTasks } from '../services/storage';
import { createTask, fetchTasksFromAPI } from '../services/api';
type TaskAction =
| { type: 'LOAD_TASKS'; payload: Task[] }
| { type: 'ADD_TASK'; payload: Task }
| { type: 'UPDATE_TASK'; payload: Task }
| { type: 'DELETE_TASK'; payload: string }
| { type: 'TOGGLE_TASK'; payload: string }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null };
interface TaskState {
tasks: Task[];
isLoading: boolean;
error: string | null;
}
const initialState: TaskState = {
tasks: [],
isLoading: false,
error: null,
};
function taskReducer(state: TaskState, action: TaskAction): TaskState {
switch (action.type) {
case 'LOAD_TASKS':
return { ...state, tasks: action.payload, isLoading: false };
case 'ADD_TASK':
return { ...state, tasks: [action.payload, ...state.tasks] };
case 'UPDATE_TASK':
return {
...state,
tasks: state.tasks.map((t) =>
t.id === action.payload.id ? action.payload : t
),
};
case 'DELETE_TASK':
return {
...state,
tasks: state.tasks.filter((t) => t.id !== action.payload),
};
case 'TOGGLE_TASK':
return {
...state,
tasks: state.tasks.map((t) =>
t.id === action.payload
? {
...t,
status: t.status === 'active' ? 'completed' : 'active',
updatedAt: new Date().toISOString(),
}
: t
),
};
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, isLoading: false };
default:
return state;
}
}
export function useTasks(filter: TaskFilter) {
const [state, dispatch] = useReducer(taskReducer, initialState);
// Load tasks from local storage on mount, then sync with API
useEffect(() => {
async function initialize() {
dispatch({ type: 'SET_LOADING', payload: true });
try {
const cached = await loadTasks();
if (cached.length > 0) {
dispatch({ type: 'LOAD_TASKS', payload: cached });
}
const remote = await fetchTasksFromAPI();
dispatch({ type: 'LOAD_TASKS', payload: remote });
await saveTasks(remote);
} catch (err) {
dispatch({
type: 'SET_ERROR',
payload: err instanceof Error ? err.message : 'Failed to load tasks',
});
}
}
initialize();
}, []);
// Persist to local storage whenever tasks change
useEffect(() => {
if (state.tasks.length > 0) {
saveTasks(state.tasks).catch(console.error);
}
}, [state.tasks]);
const filteredTasks = state.tasks.filter((task) => {
if (filter.status !== 'all' && task.status !== filter.status) return false;
if (filter.priority !== 'all' && task.priority !== filter.priority) return false;
return true;
});
const addTask = useCallback(
async (data: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>) => {
const newTask = await createTask(data);
dispatch({ type: 'ADD_TASK', payload: newTask });
},
[]
);
const toggleTask = useCallback((id: string) => {
dispatch({ type: 'TOGGLE_TASK', payload: id });
}, []);
const deleteTask = useCallback((id: string) => {
dispatch({ type: 'DELETE_TASK', payload: id });
}, []);
return {
tasks: filteredTasks,
isLoading: state.isLoading,
error: state.error,
addTask,
toggleTask,
deleteTask,
counts: {
all: state.tasks.length,
active: state.tasks.filter((t) => t.status === 'active').length,
completed: state.tasks.filter((t) => t.status === 'completed').length,
},
};
}
Why useReducer Over useState for Complex State
When state transitions involve multiple related values that change together, or when the next state depends on the previous state in non-trivial ways, useReducer gives you clearer, more testable logic. Each action type is explicit and type-safe, meaning TypeScript will catch misspelled action types at compile time. The reducer function is also pure and can be unit tested completely independently of React – just call taskReducer(state, action) and assert on the return value.
When to Reach for External State Management
React’s built-in hooks cover most scenarios, but two situations warrant an external library. First, when deeply nested components need to read or update shared state and prop drilling becomes unwieldy – use Zustand 5.x for its minimal API and excellent TypeScript support. Second, when you have complex server state with caching, background refetching, and optimistic updates – use TanStack Query 5.x, which works identically in React Native and React web. Both libraries are well-maintained and compatible with React Native 0.84 and the New Architecture.
Step 6: Adding Navigation with React Navigation
Navigation is where React Native apps diverge most sharply from web apps. There is no browser history API, no URL bar, and gestures need to feel native on each platform. React Navigation remains the most widely used navigation library in the ecosystem, and its file-based routing integration via Expo Router significantly reduces boilerplate in this react native expo tutorial.
Configuring the Root Layout
// app/_layout.tsx
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
export default function RootLayout() {
return (
<>
<Stack
screenOptions={{
headerStyle: { backgroundColor: '#6366f1' },
headerTintColor: '#ffffff',
headerTitleStyle: { fontWeight: '700' },
animation: 'slide_from_right',
}}
>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="task/new"
options={{
title: 'New Task',
presentation: 'modal',
}}
/>
<Stack.Screen
name="task/[id]"
options={{ title: 'Edit Task' }}
/>
</Stack>
<StatusBar style="light" />
</>
);
}
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Text } from 'react-native';
export default function TabsLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#6366f1',
tabBarInactiveTintColor: '#9ca3af',
tabBarStyle: { borderTopWidth: 1, borderTopColor: '#e5e7eb' },
headerStyle: { backgroundColor: '#6366f1' },
headerTintColor: '#ffffff',
headerTitleStyle: { fontWeight: '700' },
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Tasks',
tabBarIcon: ({ color }) => <Text style={{ color, fontSize: 20 }}>📋</Text>,
}}
/>
<Tabs.Screen
name="settings"
options={{
title: 'Settings',
tabBarIcon: ({ color }) => <Text style={{ color, fontSize: 20 }}>⚙️</Text>,
}}
/>
</Tabs>
);
}
Programmatic Navigation and Deep Linking
Expo Router builds a URL scheme for your app automatically. Every screen maps to a URL, which means deep linking – opening a specific task from a push notification, for example – works with zero extra configuration. Use the useRouter hook for programmatic navigation and the useLocalSearchParams hook to read route parameters on the task detail screen:
// app/task/[id].tsx
import { useLocalSearchParams, useRouter } from 'expo-router';
import { View, Text, Pressable, StyleSheet } from 'react-native';
export default function TaskDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const handleSave = async () => {
// ... save logic
router.back();
};
const handleDelete = () => {
router.replace('/(tabs)/');
};
return (
<View style={styles.container}>
<Text style={styles.idText}>Editing task: {id}</Text>
{/* TaskForm component goes here */}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: '#ffffff' },
idText: { color: '#6b7280', fontSize: 12, marginBottom: 16 },
});
For teams building complex navigation hierarchies, our Next.js full-stack tutorial covers file-based routing patterns that map directly to how Expo Router works – the mental models transfer cleanly between web and mobile.
Navigation Performance Optimization
Enable react-native-screens to replace React Native’s pure-JavaScript views with native screen containers. This dramatically reduces memory usage when navigating between screens because the native layer can properly deallocate off-screen views. Add enableScreens() from react-native-screens at the top of your root _layout.tsx, before any navigation code. Expo Router configures this automatically when you install the package via npx expo install react-native-screens.
Step 7: Connecting to a Backend API
The Task Manager syncs with a REST API. In production you would use your own backend, but for this react native tutorial we target a local Express server or a hosted mock API. The service layer in services/api.ts encapsulates all network logic, keeping it out of components and hooks and making it straightforward to test in isolation.
Building the API Service
// services/api.ts
import { Task } from '../types/task';
const BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? 'https://api.example.com';
class ApiError extends Error {
constructor(
message: string,
public readonly statusCode: number
) {
super(message);
this.name = 'ApiError';
}
}
async function request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${BASE_URL}${endpoint}`;
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
throw new ApiError(
`Request failed: ${response.statusText}`,
response.status
);
}
return response.json() as Promise<T>;
}
export async function fetchTasksFromAPI(): Promise<Task[]> {
return request<Task[]>('/tasks');
}
export async function createTask(
data: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>
): Promise<Task> {
return request<Task>('/tasks', {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updateTask(
id: string,
data: Partial<Omit<Task, 'id' | 'createdAt'>>
): Promise<Task> {
return request<Task>(`/tasks/${id}`, {
method: 'PATCH',
body: JSON.stringify({ ...data, updatedAt: new Date().toISOString() }),
});
}
export async function deleteTask(id: string): Promise<void> {
return request<void>(`/tasks/${id}`, { method: 'DELETE' });
}
Set EXPO_PUBLIC_API_URL in a .env.local file at the project root. Expo automatically exposes environment variables prefixed with EXPO_PUBLIC_ to your JavaScript bundle – variables without this prefix stay server-side only. Never put secrets like API keys in EXPO_PUBLIC_ variables; they will be embedded in the app bundle and can be extracted by anyone who downloads your app.
Handling Network Errors Gracefully
Mobile apps operate in environments with unreliable connectivity. Always implement a strategy for offline scenarios: show cached data when the network is unavailable, queue mutations for retry when connectivity returns, and give users clear feedback with non-blocking toasts rather than blocking error screens. The @react-native-community/netinfo library provides real-time connectivity status that you can combine with a simple retry queue. When the device reconnects, flush the queue in order, handling conflicts between local and remote state using timestamps in the updatedAt field.
Step 8: Implementing Local Storage with AsyncStorage
AsyncStorage is React Native’s equivalent of the browser’s localStorage – a simple key-value store that persists data across app sessions. In this react native 2026 tutorial we use it as a caching layer so the app feels instant on launch even before the API responds. The pattern is cache-first: show local data immediately, then update with the API response when it arrives.
// services/storage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Task } from '../types/task';
const TASKS_KEY = '@taskmanager/tasks';
const SCHEMA_VERSION_KEY = '@taskmanager/schema_version';
const CURRENT_SCHEMA_VERSION = 2;
export async function loadTasks(): Promise<Task[]> {
try {
const version = await AsyncStorage.getItem(SCHEMA_VERSION_KEY);
if (Number(version) < CURRENT_SCHEMA_VERSION) {
// Schema changed: clear stale data and set new version
await AsyncStorage.removeItem(TASKS_KEY);
await AsyncStorage.setItem(
SCHEMA_VERSION_KEY,
String(CURRENT_SCHEMA_VERSION)
);
return [];
}
const raw = await AsyncStorage.getItem(TASKS_KEY);
if (raw === null) return [];
const tasks = JSON.parse(raw) as Task[];
return Array.isArray(tasks) ? tasks : [];
} catch (error) {
console.error('[Storage] Failed to load tasks:', error);
return [];
}
}
export async function saveTasks(tasks: Task[]): Promise<void> {
try {
await AsyncStorage.multiSet([
[TASKS_KEY, JSON.stringify(tasks)],
[SCHEMA_VERSION_KEY, String(CURRENT_SCHEMA_VERSION)],
]);
} catch (error) {
console.error('[Storage] Failed to save tasks:', error);
throw error;
}
}
The schema versioning pattern shown above is critical for production apps. When you change your data shape in an update, users running the old schema will get a clean slate rather than a crash from parsing incompatible JSON. Always increment CURRENT_SCHEMA_VERSION when you make breaking changes to stored data structures, and implement a migration function for non-breaking changes where you want to preserve user data.
AsyncStorage Limits and Alternatives
AsyncStorage has a 6MB default limit on Android (configurable) and is not suitable for large binary data or relational queries. For larger datasets, consider WatermelonDB (high-performance relational storage built on SQLite) or the expo-sqlite package which was significantly improved in Expo SDK 53 and further stabilized in SDK 55. For sensitive data like auth tokens, use expo-secure-store which writes to iOS Keychain and Android Keystore – not AsyncStorage, which is unencrypted on Android.
Optimistic Updates for Responsive UX
For the best user experience, apply state changes optimistically – update the local state immediately when the user acts, then confirm or roll back once the API responds. In the task reducer, when a user toggles a task complete, dispatch TOGGLE_TASK immediately (updating the UI instantly) and then call the API in the background. If the API call fails, dispatch a TOGGLE_TASK again to revert the change and show an error notification. This pattern makes the app feel fast even on slow connections.
Step 9: Testing Your React Native App
Testing in react native app development spans three layers: unit tests for reducers and utility functions, component tests with React Native Testing Library (RNTL), and end-to-end tests with Maestro or Detox. A healthy test suite invests most effort in the component layer, which gives the best signal-to-maintenance ratio.
Setting Up Jest and React Native Testing Library
# Install testing dependencies
npm install --save-dev jest @testing-library/react-native @testing-library/jest-native jest-expo
# jest.config.js is auto-configured by expo — verify in package.json:
# "jest": { "preset": "jest-expo" }
# Run tests in watch mode during development
npx jest --watch
# Run once with coverage (for CI)
npx jest --ci --coverage
Writing a Component Test
// __tests__/TaskCard.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import { TaskCard } from '../components/TaskCard';
import { Task } from '../types/task';
const mockTask: Task = {
id: 'task-001',
title: 'Write unit tests',
description: 'Cover all critical paths',
status: 'active',
priority: 'high',
dueDate: '2026-04-01T00:00:00.000Z',
createdAt: '2026-03-01T09:00:00.000Z',
updatedAt: '2026-03-01T09:00:00.000Z',
};
describe('TaskCard', () => {
it('renders task title and description', () => {
render(
<TaskCard
task={mockTask}
onPress={jest.fn()}
onToggleComplete={jest.fn()}
/>
);
expect(screen.getByText('Write unit tests')).toBeTruthy();
expect(screen.getByText('Cover all critical paths')).toBeTruthy();
});
it('calls onToggleComplete when checkbox is pressed', () => {
const onToggle = jest.fn();
render(
<TaskCard
task={mockTask}
onPress={jest.fn()}
onToggleComplete={onToggle}
/>
);
const checkbox = screen.getByRole('checkbox');
fireEvent.press(checkbox);
expect(onToggle).toHaveBeenCalledWith('task-001');
expect(onToggle).toHaveBeenCalledTimes(1);
});
it('applies completed styling when task status is completed', () => {
const completedTask = { ...mockTask, status: 'completed' as const };
render(
<TaskCard
task={completedTask}
onPress={jest.fn()}
onToggleComplete={jest.fn()}
/>
);
const title = screen.getByText('Write unit tests');
const titleStyle = title.props.style;
const flatStyle = Array.isArray(titleStyle)
? Object.assign({}, ...titleStyle)
: titleStyle;
expect(flatStyle.textDecorationLine).toBe('line-through');
});
});
// Output after running npx jest:
//
// PASS __tests__/TaskCard.test.tsx
// TaskCard
// ✓ renders task title and description (42 ms)
// ✓ calls onToggleComplete when checkbox is pressed (18 ms)
// ✓ applies completed styling when task status is completed (14 ms)
//
// Test Suites: 1 passed, 1 total
// Tests: 3 passed, 3 total
// Time: 1.847 s
Run your tests in CI on every pull request. Our GitHub Actions CI/CD tutorial covers setting up a pipeline that runs Jest, type checking, and ESLint automatically – pair it with this guide to get professional-grade automation from day one.
Testing the Task Reducer
Pure reducer functions are the easiest things to test in the entire codebase – no mocks, no renders, just function calls and assertions. Write a test for every action type and pay special attention to edge cases: toggling an already-completed task, deleting a task that does not exist in state, and loading tasks into a non-empty state. These tests run in milliseconds and catch regressions immediately.
Step 10: Building and Deploying to App Stores
Deploying a React Native app has historically been one of the most painful parts of the process. Expo Application Services (EAS) solves this by handling code signing, build infrastructure, and submission workflows in the cloud. You do not need a Mac to build iOS apps with EAS – Expo’s servers run the Xcode build for you.
Configuring EAS Build
# Log in to your Expo account
eas login
# Initialize EAS in your project (creates eas.json)
eas build:configure
# eas.json — customize build profiles:
{
"cli": { "version": ">= 14.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": true }
},
"preview": {
"distribution": "internal",
"android": { "buildType": "apk" }
},
"production": {
"autoIncrement": true,
"android": { "buildType": "app-bundle" },
"ios": { "credentialsSource": "remote" }
}
},
"submit": {
"production": {
"android": {
"serviceAccountKeyPath": "./google-service-account.json",
"track": "production"
},
"ios": {
"appleId": "[email protected]",
"ascAppId": "1234567890"
}
}
}
}
Running Production Builds and Submission
# Build for both platforms simultaneously
eas build --platform all --profile production
# Submit to stores after successful build
eas submit --platform all --profile production
# Or submit a specific build by ID
eas submit --platform ios --id <build-id>
# Push an OTA update to the production channel (JS-only changes)
eas update --branch production --message "Fix task due date display bug"
# Output:
# ✔ Published!
# Branch: production
# Runtime version: 1.0.0
# Update group ID: abc123-def456
# Platforms: android, ios
EAS Build queues your build on managed infrastructure and emails you when it completes. A production iOS build typically takes 12–20 minutes; Android takes 8–15 minutes. With the autoIncrement flag, the build number increments automatically so you never have to manually update version numbers before a release.
Over-the-Air Updates with EAS Update
EAS Update lets you push JavaScript-only changes to production without going through App Store review. Because the New Architecture runs all JavaScript on Hermes V1, updates are fully compatible. Use OTA updates for bug fixes and UI changes; submit a full build for any change that touches native modules or app configuration. Apple and Google’s guidelines permit OTA updates for JavaScript-layer changes as long as you do not use them to fundamentally alter the app’s purpose – a restriction that matters for game mechanics and gambling apps but not for routine bug fixes and feature improvements.
Performance Optimization and Advanced Tips
The New Architecture in React Native 0.84 delivers dramatic benchmark improvements over the legacy bridge: cold start time is down 43%, rendering throughput is up 39%, and memory usage is down 26%. But these gains are the baseline – poorly written components can still cause jank on lower-end Android devices. Here are the techniques that matter most for production react native app development in 2026.
FlatList Optimization for Long Task Lists
FlatList virtualizes its items – only items near the viewport are mounted and rendered. To maximize this benefit, always provide a stable keyExtractor, use getItemLayout when items have fixed heights (avoids layout measurement on every item), and memoize the renderItem function with useCallback. Wrap item components with React.memo to prevent unnecessary re-renders when sibling items change. Also set initialNumToRender to the number of items visible on screen (typically 8–12) to avoid rendering off-screen items on initial load.
| Optimization Technique | Impact | When to Use | Complexity |
|---|---|---|---|
| React.memo on list items | High | Always for FlatList items | Low |
| useCallback for handlers | Medium | When passing callbacks to memoized children | Low |
| getItemLayout | High | Fixed-height list items | Low |
| removeClippedSubviews | Medium | Very long lists (500+ items) | Low |
| Hermes profiling | Variable | Identifying specific bottlenecks | Medium |
| useNativeDriver animations | High | All opacity/transform animations | Low |
| InteractionManager.runAfterInteractions | Medium | Heavy work after navigation transitions | Low |
| Reanimated 3 worklets | Very High | Complex gesture-driven animations | Medium |
Hermes V1 and the New Architecture
Hermes V1 (default since React Native 0.70, significantly improved in 0.84) compiles JavaScript to bytecode at build time, eliminating the JIT compilation step on device startup. Combined with JSI (JavaScript Interface) – which replaces the asynchronous JSON bridge with synchronous C++ function calls – native module calls no longer block the JS thread. You can call native APIs directly from JavaScript at 60fps without batching or async queues.
To profile your app, open the Hermes profiler in the React Native DevTools – the tool that replaced Flipper as the default debugger when the React Native core team announced the switch alongside 0.76.0 on October 23, 2024 (press j in the Metro terminal to open the debugger). The flame graph view shows exactly which JavaScript functions are consuming frame budget. Pair this with the AI coding tools guide – modern AI-powered IDEs can now suggest performance optimizations based on profiling data automatically, cutting the time from “identified bottleneck” to “deployed fix” significantly.
Web Performance APIs in React Native 0.84
The stabilized Web Performance APIs mean you can use performance.now(), PerformanceObserver, and performance.mark() directly in your React Native app – the same APIs you would use for web performance monitoring. This simplifies cross-platform performance instrumentation because the same measurement code works on web (via Next.js) and mobile without any abstraction layer. React Native 0.84 also ships the Intersection Observer API in Canary, which will enable lazy loading patterns for off-screen components once it stabilizes in 0.85.
Common Pitfalls and Troubleshooting
Even experienced React developers hit platform-specific walls when learning this react native tutorial. The following pitfalls and troubleshooting items cover the issues most commonly raised in the React Native GitHub discussions and community forums in 2025–2026.
Pitfall 1: Mutating State Directly in useReducer
React’s state update detection is based on reference equality. If you mutate an existing object or array in a reducer and return it, React will not detect the change and will not re-render. Always return a new object or array from your reducer. Use the spread operator, Array.prototype.map, Array.prototype.filter, or a library like Immer if your state is deeply nested. This is not React Native-specific, but it is responsible for a disproportionate number of “why isn’t my UI updating” bug reports from developers new to the framework.
Pitfall 2: Inline Style Objects Breaking Memoization
Every time the parent component re-renders, inline style objects are recreated as new references. When passed to a memoized component, this breaks memoization – React.memo sees new props on every render and re-renders every list item. Move all styles into StyleSheet.create() and pass the style IDs instead. The only styles that should be inline are computed runtime values like {{ backgroundColor: task.priority === 'high' ? '#ef4444' : '#22c55e' }}.
Pitfall 3: Text Overflow in Flexbox Rows
A common layout bug: you have a row with an icon on the left and text on the right, and the text overflows the container instead of wrapping. The fix is to add flex: 1 or flexShrink: 1 to the text’s container View. Without it, the View grows to fit its text content and ignores the parent flexbox constraints. This is one of the most-googled React Native layout issues, and the fix is always the same.
Pitfall 4: useEffect Without a Dependency Array Causing Infinite Loops
A useEffect without a dependency array runs on every render. If it triggers a state update, it causes another render, which triggers the effect again – creating an infinite loop that will crash your app. Always specify a dependency array. Use the ESLint rule react-hooks/exhaustive-deps (included in the Expo ESLint config) to catch missing dependencies automatically. When you genuinely need an effect to run only once on mount, use an empty dependency array [].
Pitfall 5: Ignoring Platform Differences in Shadows
iOS uses shadowColor, shadowOffset, shadowOpacity, and shadowRadius for shadows. Android uses elevation. Both are required if you want consistent card shadows across platforms. Setting only the iOS shadow props and wondering why your Android cards look flat is one of the most common UI bugs in React Native apps. A simple cross-platform shadow utility wrapping Platform.select() removes this footgun permanently.
Pitfall 6: Storing Sensitive Data in AsyncStorage
AsyncStorage is unencrypted on Android. Storing authentication tokens, passwords, or personal user data in AsyncStorage is a security vulnerability. Use expo-secure-store for any sensitive values – it writes to the iOS Keychain and Android Keystore, both of which provide hardware-backed encryption on modern devices. AsyncStorage is appropriate only for non-sensitive cached data like task lists, preferences, and analytics flags.
Troubleshooting Guide
- Metro bundler fails with “Cannot find module” error: Clear the Metro cache with
npx expo start --clear. If the error persists, deletenode_modulesand the.expodirectory, then runnpm installand restart. - iOS simulator shows a blank white screen on launch: A JavaScript error is crashing before any UI renders. Check the Metro terminal for a red error box, or shake the device to open the developer menu and view crash logs. Check that all imports resolve correctly.
- Android build fails with “Execution failed for task ‘:app:mergeDebugResources'”: Conflicting resource names between libraries. Run
./gradlew app:dependenciesin theandroid/directory to identify conflicting dependency versions, then align them in yourpackage.json. - Expo Go shows “Something went wrong” after updating dependencies: Your updated packages may require a newer Expo Go version or include native modules unsupported in the managed workflow. Run
npx expo-doctorto identify incompatibilities and the recommended resolution. - Keyboard covers input fields on Android: Set
android:windowSoftInputMode="adjustResize"in yourAndroidManifest.xml(or use thesoftwareKeyboardLayoutModefield inapp.json), and wrap your screen content in aKeyboardAvoidingViewwithbehavior="height"on Android. - FlatList performance degrades with many items: Enable
removeClippedSubviews, setmaxToRenderPerBatch={10},windowSize={5}, andinitialNumToRender={10}. Ensure renderItem is wrapped in useCallback and item components use React.memo. - TypeScript errors after upgrading React Native or Expo: Run
npx expo install --fixto align all package versions to the Expo SDK 55 compatibility matrix, then re-runnpx tsc --noEmitto surface any remaining type errors. - Network requests fail on Android emulator but work on iOS: Android emulators use
10.0.2.2to refer to the host machine’s localhost. Change yourEXPO_PUBLIC_API_URLtohttp://10.0.2.2:3000for local development on Android, or use a conditional based onPlatform.OS. - EAS Build fails with “missing credentials”: Run
eas credentialsto interactively configure iOS certificates and provisioning profiles. For Android, ensure your keystore is either stored in EAS or committed to your project (testing only – never commit production keystores). - App crashes on startup after an OTA update: Implement a rollback strategy using
Updates.checkForUpdateAsync()and a try-catch aroundUpdates.reloadAsync(). Expo’s runtime will automatically fall back to the last known-good bundle on a corrupt update, but proactive version checking in your app gives you additional control.
React Native 2026 Ecosystem Overview
The react native 2026 ecosystem is more cohesive than ever. The Meta engineering team has kept up its quarterly release cadence, and as of September 2026 the framework stands at 0.87.1 – released August 26, 2026 per GitHub and now the latest stable tag on both GitHub and the official docs, having overtaken 0.86 as current that same month – after the 0.87 release made the Strict TypeScript API the default and added experimental Swift Package Manager support and an Android Gradle Plugin 9 requirement alongside a fresh round of DevTools improvements, per the React Native blog. That follows 0.86, which had already brought Android 15 edge-to-edge support and whose release branch cut on June 9, 2026, with its 0.86.3 patch still listed as actively supported by the endoflife.date tracker, and 0.85, which arrived in April 2026 with the New Architecture finally graduating to stable before picking up its 0.85.3 patch in May 2026, per endoflife.date. That momentum is not new – 0.76.0 alone landed 1,491 commits from 165 contributors when it shipped in October 2024, and per the Agilesoftlabs migration guides updated in May 2026, teams upgrading onto the 0.76 line now need to pin React to 19.0.0 or later in package.json to pick up full React 19 support. The community has largely converged on a standard stack, reducing the framework-choice decision fatigue that plagued projects in prior years. React 19.2, integrated since the December 2025 React Native 0.83 release (which itself reached its 0.83.10 patch in June 2026), brings improved concurrent features, better automatic batching, and the new use() hook for async data – all of which improve the developer experience in ways that were not possible under the old bridge architecture.
| Category | Recommended Library | Version (2026) | Notes |
|---|---|---|---|
| Navigation | Expo Router / React Navigation | Router v4 / Nav v7 | File-based routing preferred for new projects |
| State Management | React built-in hooks + Zustand | Zustand 5.x | Redux Toolkit for large teams |
| Animations | React Native Reanimated | 3.x | Worklets run on UI thread, no bridge overhead |
| Forms | React Hook Form | 7.x | Excellent TypeScript support |
| Data Fetching | TanStack Query | 5.x | Handles caching, retries, and background sync |
| Local DB | expo-sqlite or WatermelonDB | SDK 55 / 0.27 | WatermelonDB for relational data needs |
| Styling | StyleSheet + NativeWind | NativeWind 4.x | Tailwind-compatible utility classes |
| Testing | Jest + RNTL + Maestro | RNTL 12.x | Maestro for E2E on real devices |
The official React Native documentation at reactnative.dev and the Expo documentation at docs.expo.dev are both excellent and actively maintained. Beyond the official docs, structured video courses remain popular: Coursera’s “The Complete React Native Bootcamp 2026” specialization packs in 24 hours of video content as of May 2026, and Class Central noted in July 2026 that a comparable membership-based course runs $9/month on an annual plan versus $23/month billed monthly. The React core documentation at react.dev covers hooks, concurrent features, and the APIs now shipping in React 19.3.0 – which launched September 9, 2026 and is the release most relevant to current React Native tutorials – in depth, all of which apply equally to React Native since the version 0.83+ React 19.2 integration that preceded it.
When comparing framework options, our Flutter vs React Native 2026 comparison walks through benchmarks, ecosystem maturity, hiring market data, and use-case recommendations. If your mobile app is part of a larger product with a web frontend, our Next.js full-stack tutorial shows how to share business logic and TypeScript definitions between a React Native app and a Next.js app using a monorepo structure.
Frequently Asked Questions
Is React Native still relevant in 2026, or should I learn Flutter instead?
Both frameworks are actively used in production at scale, and both are strong choices in 2026. React Native has the advantage if your team has existing JavaScript/TypeScript expertise, if you want to share code with a React web frontend, or if your app relies heavily on the JavaScript/npm ecosystem. Flutter has advantages for pixel-perfect custom UIs and teams coming from a mobile-native background. See our Flutter vs React Native 2026 comparison for detailed benchmark data and use-case analysis.
What is the New Architecture and do I need to worry about it?
The New Architecture is a complete rewrite of React Native’s internal communication layer, replacing the asynchronous JSON bridge with JSI, Fabric, and TurboModules. It has been the default since React Native 0.76, and the old bridge was permanently removed in 0.82. If you start a new project with React Native 0.84, you are already on the New Architecture automatically. The main practical implication: any third-party native modules that have not been updated to support the New Architecture will not work. Check the repository’s README or issues for “New Architecture” or “JSI” support before installing a native module.
Should I use Expo or the React Native CLI for a new project?
For the vast majority of new projects in 2026, Expo is the right choice. The managed workflow handles native configuration automatically, EAS Build eliminates the need for a Mac for iOS builds, and Expo SDK 55 covers nearly every native capability a typical app needs. The only reason to start with the bare React Native CLI is if you have existing custom native modules written in Objective-C/Swift or Kotlin/Java that you need to integrate from day one. You can always migrate an Expo managed project to a bare workflow later without losing your JavaScript codebase.
How do I share code between my React Native app and a Next.js website?
The standard approach is a monorepo with a shared packages/ directory containing platform-agnostic code: TypeScript types, business logic, API clients, utility functions, and constants. Tools like Turborepo or Nx manage the build graph. Both your Expo app and your Next.js app import from the shared package. Our Next.js full-stack tutorial covers the monorepo setup in detail. Avoid sharing UI components across platforms – the differences between React Native’s StyleSheet system and web CSS mean shared UI components are almost always more trouble than they are worth.
Do I need TypeScript, or can I use plain JavaScript?
You can use plain JavaScript, but TypeScript is strongly recommended for any project beyond a simple prototype. TypeScript catches undefined property access, wrong function argument types, and missing required props at compile time rather than at runtime on a user’s device. The Expo TypeScript template is the default starting point and all major ecosystem libraries ship TypeScript types. If you are new to TypeScript, our TypeScript tutorial covers what you need in a single focused session.
How much does it cost to publish to the App Store and Google Play?
Apple charges $99 USD per year for an Apple Developer Program membership, required for App Store distribution including TestFlight. Google charges a one-time $25 USD registration fee for the Google Play Console. EAS Build has a free tier covering a reasonable number of builds per month; paid tiers start at $29/month for more concurrent builds or higher priority. Both app stores take a 30% revenue share on paid apps and in-app purchases (15% for the first $1M annually under Apple’s Small Business Program and Google Play’s comparable offering).
What are Over-the-Air updates and when should I use them?
OTA updates let you push JavaScript and asset changes to users without going through App Store review – the update is downloaded in the background and applied on the next app launch. Use them for bug fixes, content changes, and UI tweaks. You cannot use OTA updates for native code changes (new native modules, modified app permissions, or native configuration changes). Apple and Google’s guidelines permit JavaScript-layer OTA updates; you cannot use them to change the fundamental purpose of the app. EAS Update is the Expo-managed OTA solution and integrates smoothly with the same CLI and CI/CD pipeline used for full builds.
How do I add AI features to my React Native app in 2026?
The most common pattern is to call an AI API (OpenAI, Anthropic, Google Gemini) from your backend and surface the results via your existing REST API – the React Native app treats AI responses the same as any other API data. For on-device inference, ExecuTorch (Meta’s mobile inference runtime) runs quantized models directly on the device with no network round-trip, suitable for text classification, image analysis, and small language models on modern hardware. The AI coding tools guide covers the broader ecosystem of AI integrations available to mobile developers today.
This react native tutorial has covered the complete journey from environment setup through App Store deployment for a production-quality Task Manager application. The patterns shown here – TypeScript throughout, useReducer for complex state, the service layer for API and storage, React Native Testing Library for component tests, and EAS for builds and submission – represent the professional standard for react native app development in 2026.
React Native, now at 0.87.1 (released August 26, 2026) and current as of September 2026 – with the 0.87 release having made the Strict TypeScript API the default, added experimental Swift Package Manager support, raised the Node.js floor to 22, and required Android Gradle Plugin 9, on top of the Android 15 edge-to-edge support 0.86 already shipped – and the New Architecture together make for a genuinely strong platform. The 43% cold start improvement, 39% rendering throughput gain, and 26% memory reduction compared to the bridge era make it competitive with native development for the vast majority of app categories. With React now at 19.3.0 following its September 9, 2026 launch, Hermes V1, Expo SDK 55, and a quarterly release cadence that has already carried the framework from 0.84.1 in February 2026 through 0.85 (and its 0.85.3 patch) in April and May, 0.86 in June, and on to 0.87.1 by August 2026, the framework is in the best shape of its history.
To continue building on what you have learned here, work through our TypeScript tutorial for stronger typing patterns, our GitHub Actions CI/CD tutorial for automated build pipelines, and our AI coding tools guide to accelerate your development workflow. The complete Task Manager source code built throughout this build mobile app react native tutorial is structured to serve as a production-ready template you can clone and extend for your own projects.





