React Native with Expo has become the go-to framework for building production-ready mobile apps in 2026. With Expo SDK 53, the New Architecture enabled by default, and seamless EAS Build integration, you can ship polished iOS and Android apps from a single TypeScript codebase faster than ever. In this guide, we’ll build a practical task manager app step by step, covering navigation, state management, and native device features.
Why React Native + Expo in 2026?
The mobile development landscape has shifted dramatically. Expo is no longer a “beginner tool” — it’s the recommended way to build React Native apps, even by the React Native team. Here’s what makes it compelling:
- Expo Router v4 — file-based routing inspired by Next.js, with deep linking out of the box
- New Architecture — JSI-based bridge replacement for near-native performance
- EAS Build & Submit — cloud builds and app store submissions without Xcode or Android Studio
- Expo Modules API — write native modules in Swift/Kotlin with a clean TypeScript interface
- Universal apps — target iOS, Android, and web from the same codebase
Setting Up Your Project
First, create a new Expo project with the latest template:
npx create-expo-app@latest TaskFlow --template tabs
cd TaskFlow
npx expo startThis scaffolds a project with Expo Router, TypeScript, and tab navigation preconfigured. Your project structure looks like this:
TaskFlow/
├── app/
│ ├── (tabs)/
│ │ ├── index.tsx # Home tab
│ │ ├── settings.tsx # Settings tab
│ │ └── _layout.tsx # Tab layout
│ ├── task/
│ │ └── [id].tsx # Dynamic task detail route
│ └── _layout.tsx # Root layout
├── components/
├── hooks/
└── constants/Building the Task List Screen
Let’s build the main task list using React Native’s FlatList and Zustand for state management. First, install Zustand:
npx expo install zustandCreate a store at store/taskStore.ts:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface Task {
id: string;
title: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
createdAt: number;
}
interface TaskStore {
tasks: Task[];
addTask: (title: string, priority: Task['priority']) => void;
toggleTask: (id: string) => void;
deleteTask: (id: string) => void;
}
export const useTaskStore = create<TaskStore>()(
persist(
(set) => ({
tasks: [],
addTask: (title, priority) =>
set((state) => ({
tasks: [
{
id: Date.now().toString(),
title,
completed: false,
priority,
createdAt: Date.now(),
},
...state.tasks,
],
})),
toggleTask: (id) =>
set((state) => ({
tasks: state.tasks.map((t) =>
t.id === id ? { ...t, completed: !t.completed } : t
),
})),
deleteTask: (id) =>
set((state) => ({
tasks: state.tasks.filter((t) => t.id !== id),
})),
}),
{
name: 'task-storage',
storage: createJSONStorage(() => AsyncStorage),
}
)
);Now build the task list screen at app/(tabs)/index.tsx:
import { FlatList, Pressable, StyleSheet, View } from 'react-native';
import { Text } from '@/components/Themed';
import { useTaskStore } from '@/store/taskStore';
import { Link } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
const priorityColors = {
high: '#ef4444',
medium: '#f59e0b',
low: '#22c55e',
};
export default function HomeScreen() {
const { tasks, toggleTask, deleteTask } = useTaskStore();
return (
<View style={styles.container}>
<FlatList
data={tasks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Pressable
style={styles.taskRow}
onPress={() => toggleTask(item.id)}
>
<View style={[
styles.priorityDot,
{ backgroundColor: priorityColors[item.priority] }
]} />
<Text style={[
styles.taskTitle,
item.completed && styles.completed
]}>
{item.title}
</Text>
<Pressable onPress={() => deleteTask(item.id)}>
<Ionicons name="trash-outline" size={20} color="#888" />
</Pressable>
</Pressable>
)}
ListEmptyComponent={
<Text style={styles.empty}>No tasks yet. Tap + to add one.</Text>
}
/>
<Link href="/task/new" asChild>
<Pressable style={styles.fab}>
<Ionicons name="add" size={28} color="#fff" />
</Pressable>
</Link>
</View>
);
}Adding Haptic Feedback and Notifications
Native features are where Expo shines. Let’s add haptic feedback when completing tasks and schedule local notifications for reminders:
npx expo install expo-haptics expo-notificationsEnhance the toggle function with haptics:
import * as Haptics from 'expo-haptics';
const handleToggle = (id: string) => {
toggleTask(id);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};Schedule a reminder notification:
import * as Notifications from 'expo-notifications';
async function scheduleReminder(taskTitle: string, minutes: number) {
await Notifications.scheduleNotificationAsync({
content: {
title: 'Task Reminder 📋',
body: `Don't forget: ${taskTitle}`,
sound: true,
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
seconds: minutes * 60,
},
});
}Swipe-to-Delete with Reanimated
For a polished UX, add swipe gestures using react-native-gesture-handler and react-native-reanimated (both included with Expo):
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
runOnJS,
} from 'react-native-reanimated';
function SwipeableTask({ task, onDelete }) {
const translateX = useSharedValue(0);
const panGesture = Gesture.Pan()
.onUpdate((e) => {
if (e.translationX < 0) {
translateX.value = e.translationX;
}
})
.onEnd((e) => {
if (e.translationX < -120) {
translateX.value = withTiming(-400, {}, () => {
runOnJS(onDelete)(task.id);
});
} else {
translateX.value = withTiming(0);
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.taskRow, animatedStyle]}>
<Text>{task.title}</Text>
</Animated.View>
</GestureDetector>
);
}Building and Deploying with EAS
The biggest advantage of Expo in 2026 is EAS (Expo Application Services). No need for Xcode or Android Studio on your machine:
# Install EAS CLI
npm install -g eas-cli
# Configure your project
eas build:configure
# Build for both platforms
eas build --platform all --profile production
# Submit to app stores
eas submit --platform ios
eas submit --platform androidYour eas.json configuration:
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"ios": { "simulator": true }
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": { "appleId": "your@email.com" },
"android": { "serviceAccountKeyPath": "./pc-api-key.json" }
}
}
}Performance Tips for 2026
With the New Architecture now stable, here are key performance practices:
- Use
React.memoanduseCallback— prevent unnecessary re-renders in long lists - FlashList over FlatList — Shopify’s
@shopify/flash-listrenders lists 5x faster - Image optimization — use
expo-imageinstead of the defaultImagecomponent for caching and blurhash placeholders - Avoid bridge traffic — the New Architecture uses JSI, but batching state updates still matters
- Enable Hermes — it’s the default engine now, but verify with
global.HermesInternal
Wrapping Up
React Native with Expo in 2026 is a mature, production-ready platform. We’ve built a task manager with persistent state, haptic feedback, notifications, swipe gestures, and cloud deployment — all without touching Xcode or Android Studio. The ecosystem has grown significantly: Expo Router handles navigation elegantly, Zustand keeps state simple, and EAS removes the pain of app store submissions.
For your next steps, explore Expo’s universal links for deep linking, expo-secure-store for sensitive data, and React Native Skia for custom graphics. The gap between native and cross-platform has never been smaller.

Leave a Reply