React Native: Build Once, Ship Everywhere — The Complete 2025 Guide
What if you could write your mobile app once and have it run natively on both iOS and Android — without compromising on performance or user experience? That's the promise React Native has been delivering on since Meta open-sourced it in 2015. Used by companies like Meta, Microsoft, Shopify, Coinbase, and Discord, React Native has become one of the most battle-tested cross-platform mobile frameworks in the world. In 2025, with the New Architecture now stable and Expo reaching maturity, there has never been a better time to build with it.
⚛️ What Is React Native?
React Native is an open-source framework created by Meta that lets you build truly native mobile applications using JavaScript and React. Unlike hybrid frameworks that wrap a web view (like early Ionic or Cordova), React Native compiles your JavaScript components into real native UI elements — an iOS UIView or an Android View. The result is an app that looks, feels, and performs like it was written in Swift or Kotlin, because under the hood, the native components it uses are exactly the same ones those languages would produce.
The key insight behind React Native is the separation of the JavaScript thread from the native thread. Your business logic runs in JavaScript, but rendering is handled by the native platform. This bridge between the two worlds is what makes React Native powerful — and understanding it deeply is what separates good React Native developers from great ones.
🚀 Getting Started: Your First App
The fastest way to get started with React Native in 2025 is through Expo — a framework built on top of React Native that handles the toolchain, native builds, and over-the-air updates for you. It eliminates most of the painful native configuration that used to make React Native setup notoriously difficult.
# Install Expo CLI globally
npm install -g expo-cli
# Create a new React Native project
npx create-expo-app@latest my-app
# Navigate into the project
cd my-app
# Start the development server
npx expo start
# Run on iOS simulator
npx expo run:ios
# Run on Android emulator
npx expo run:android
# Or scan the QR code with Expo Go app on your physical device📁 Project Structure
my-app/
├── app/ # Expo Router — file-based routing
│ ├── (tabs)/
│ │ ├── index.tsx # Home tab screen
│ │ └── explore.tsx # Explore tab screen
│ ├── _layout.tsx # Root layout
│ └── modal.tsx # Modal screen
├── components/ # Reusable UI components
├── hooks/ # Custom React hooks
├── assets/ # Images, fonts, icons
├── constants/ # Colors, config values
├── app.json # Expo config
└── package.json🧱 Core Components: The Building Blocks
React Native doesn't use HTML elements like div or span. Instead, it provides its own set of core components that map directly to native UI elements on each platform. Mastering these is the foundation of everything you'll build.
import React, { useState } from 'react';
import {
View, // Like a div — a layout container
Text, // Like a p — all text must be in <Text>
Image, // Native image component
ScrollView, // Scrollable container
TextInput, // Native text input field
TouchableOpacity, // Pressable element with opacity feedback
FlatList, // Performant list for large datasets
StyleSheet, // CSS-in-JS for React Native
Platform, // Detect iOS vs Android
} from 'react-native';
export default function ProfileCard() {
const [liked, setLiked] = useState(false);
return (
<View style={styles.card}>
<Image
source={{ uri: 'https://i.pravatar.cc/150?img=3' }}
style={styles.avatar}
/>
<Text style={styles.name}>Sankalp Shrivastav</Text>
<Text style={styles.bio}>React Native Developer · Mumbai</Text>
<TouchableOpacity
style={[styles.button, liked && styles.buttonLiked]}
onPress={() => setLiked(!liked)}
>
<Text style={styles.buttonText}>
{liked ? '❤️ Liked' : '🤍 Like'}
</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 24,
alignItems: 'center',
margin: 16,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 10,
elevation: 4, // Android shadow
},
avatar: { width: 80, height: 80, borderRadius: 40, marginBottom: 12 },
name: { fontSize: 20, fontWeight: '700', color: '#1a1a1a' },
bio: { fontSize: 14, color: '#666', marginTop: 4, marginBottom: 16 },
button: {
backgroundColor: '#f0f0f0',
paddingHorizontal: 24,
paddingVertical: 10,
borderRadius: 20,
},
buttonLiked: { backgroundColor: '#ffe4e4' },
buttonText: { fontSize: 15, fontWeight: '600' },
});🗂️ Navigation with Expo Router
Expo Router brings file-based routing — the same pattern popularized by Next.js — to React Native. Every file inside the app/ directory automatically becomes a route. This makes navigation intuitive, deep linking effortless, and your project structure self-documenting.
// app/_layout.tsx — Root layout with tab navigation
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function RootLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#6366f1',
tabBarStyle: { backgroundColor: '#fff' },
headerShown: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explore',
tabBarIcon: ({ color, size }) => (
<Ionicons name="compass" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
// app/product/[id].tsx — Dynamic route
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';
export default function ProductScreen() {
const { id } = useLocalSearchParams();
return (
<View>
<Text>Product ID: {id}</Text>
</View>
);
}📡 Fetching Data & State Management
Most real apps need to fetch data from an API and manage complex state. In 2025, the recommended approach in React Native is TanStack Query (React Query) for server state, combined with Zustand for lightweight client state. This combination is fast to set up, powerful in production, and avoids the boilerplate overhead of Redux.
// Data fetching with TanStack Query
import { useQuery } from '@tanstack/react-query';
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';
type Post = { id: number; title: string; body: string };
const fetchPosts = async (): Promise<Post[]> => {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
if (!res.ok) throw new Error('Network response failed');
return res.json();
};
export default function PostsFeed() {
const { data: posts, isLoading, isError } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
if (isLoading) return <ActivityIndicator size="large" color="#6366f1" style={{ flex: 1 }} />;
if (isError) return <Text style={styles.error}>Failed to load posts.</Text>;
return (
<FlatList
data={posts}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<View style={styles.card}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.body} numberOfLines={2}>{item.body}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
list: { padding: 16, gap: 12 },
card: { backgroundColor: '#fff', borderRadius: 12, padding: 16, elevation: 2 },
title: { fontSize: 15, fontWeight: '700', color: '#1a1a1a', marginBottom: 6 },
body: { fontSize: 13, color: '#666', lineHeight: 20 },
error: { flex: 1, textAlign: 'center', marginTop: 40, color: 'red' },
});🔐 Authentication with Supabase
Supabase has become a go-to backend for React Native apps — providing authentication, a Postgres database, real-time subscriptions, and file storage out of the box. Here's how a clean auth flow looks with Supabase and Expo Router.
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL!,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
{
auth: {
storage: AsyncStorage, // Persist session on device
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
}
);
// app/login.tsx — Sign in with email & password
import { useState } from 'react';
import { View, TextInput, TouchableOpacity, Text, StyleSheet } from 'react-native';
import { supabase } from '../lib/supabase';
import { router } from 'expo-router';
export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const handleLogin = async () => {
setLoading(true);
const { error } = await supabase.auth.signInWithPassword({ email, password });
setLoading(false);
if (!error) router.replace('/(tabs)');
};
return (
<View style={styles.container}>
<Text style={styles.heading}>Welcome Back</Text>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity style={styles.button} onPress={handleLogin} disabled={loading}>
<Text style={styles.buttonText}>{loading ? 'Signing in...' : 'Sign In'}</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', padding: 24, backgroundColor: '#fafafa' },
heading: { fontSize: 28, fontWeight: '800', marginBottom: 32, color: '#1a1a1a' },
input: {
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 12,
padding: 14,
fontSize: 15,
marginBottom: 12,
},
button: { backgroundColor: '#6366f1', padding: 16, borderRadius: 12, alignItems: 'center' },
buttonText: { color: '#fff', fontWeight: '700', fontSize: 16 },
});🏎️ The New Architecture: JSI, Fabric & TurboModules
For years, React Native's biggest criticism was its bridge — the asynchronous JSON-serialized communication channel between JavaScript and native code. Every interaction had to be batched and passed over this bridge, causing frame drops and janky animations in complex UIs. The New Architecture, now stable as of React Native 0.74, replaces this entirely with three key innovations.
JSI (JavaScript Interface) allows JavaScript to hold direct references to native objects and call native methods synchronously — no serialization, no async round trips. Fabric is the new rendering system that computes UI layouts in C++ and enables synchronous rendering, making gestures and animations buttery smooth. TurboModules replace the old NativeModules system with lazy-loaded, type-safe native modules that are only initialized when needed, drastically improving startup time. Together, these changes bring React Native's performance ceiling dramatically closer to fully native apps.
🎨 Styling: StyleSheet, NativeWind & Tamagui
React Native's built-in StyleSheet API works well but can feel verbose for complex UIs. The ecosystem has responded with excellent alternatives. NativeWind brings Tailwind CSS utility classes to React Native — if you're already a Tailwind user on the web, this will feel immediately familiar. Tamagui is a high-performance UI kit and styling system that compiles styles at build time for near-zero runtime overhead. Both are excellent choices depending on your team's background and project needs.
// NativeWind — Tailwind classes in React Native
import { View, Text, TouchableOpacity } from 'react-native';
import { styled } from 'nativewind';
const StyledView = styled(View);
const StyledText = styled(Text);
const StyledButton = styled(TouchableOpacity);
export default function HeroCard() {
return (
<StyledView className="flex-1 items-center justify-center bg-indigo-50 p-6">
<StyledView className="bg-white rounded-2xl p-6 w-full shadow-md">
<StyledText className="text-2xl font-bold text-gray-900 mb-2">
Hello, React Native 🚀
</StyledText>
<StyledText className="text-gray-500 mb-6 leading-6">
Built with NativeWind — Tailwind classes on mobile.
</StyledText>
<StyledButton className="bg-indigo-500 rounded-xl py-4 items-center">
<StyledText className="text-white font-bold text-base">
Get Started
</StyledText>
</StyledButton>
</StyledView>
</StyledView>
);
}📦 Essential Libraries in 2025
Navigation — Expo Router (file-based) or React Navigation (programmatic). Animations — React Native Reanimated 3 for performant 60fps animations running on the UI thread. Gestures — React Native Gesture Handler for native-feeling swipes, pinches, and drags. State — Zustand for client state, TanStack Query for server state. Storage — MMKV for blazing-fast synchronous key-value storage (10x faster than AsyncStorage). Forms — React Hook Form for performant, uncontrolled form handling. Icons — @expo/vector-icons (includes Ionicons, MaterialIcons, FontAwesome). Images — Expo Image for fast, cached, and progressive image loading with blurhash support.
# Install the essential React Native stack
npx expo install \
expo-router \
react-native-reanimated \
react-native-gesture-handler \
react-native-mmkv \
@tanstack/react-query \
zustand \
react-hook-form \
expo-image \
@expo/vector-icons \
nativewind \
@supabase/supabase-js \
@react-native-async-storage/async-storage🌊 Animations with Reanimated 3
React Native Reanimated 3 is the gold standard for animations in React Native. Unlike the built-in Animated API which runs on the JavaScript thread, Reanimated executes animations directly on the UI thread using worklets — small JavaScript functions that are compiled and run natively. This means animations stay smooth at 60fps even when the JS thread is busy processing data.
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
interpolate,
} from 'react-native-reanimated';
import { TouchableOpacity, StyleSheet } from 'react-native';
export default function SpringButton() {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
const handlePressIn = () => {
scale.value = withSpring(0.92, { damping: 15, stiffness: 300 });
opacity.value = withTiming(0.8, { duration: 100 });
};
const handlePressOut = () => {
scale.value = withSpring(1, { damping: 10, stiffness: 200 });
opacity.value = withTiming(1, { duration: 150 });
};
return (
<TouchableOpacity
onPressIn={handlePressIn}
onPressOut={handlePressOut}
activeOpacity={1}
>
<Animated.View style={[styles.button, animatedStyle]}>
<Animated.Text style={styles.label}>Press Me ✨</Animated.Text>
</Animated.View>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#6366f1',
paddingHorizontal: 32,
paddingVertical: 16,
borderRadius: 16,
alignItems: 'center',
},
label: { color: '#fff', fontWeight: '700', fontSize: 16 },
});📲 Publishing: EAS Build & OTA Updates
Expo Application Services (EAS) is the production build and deployment platform for Expo apps. EAS Build runs your native builds in the cloud — no Mac required to build an iOS app. EAS Submit automates uploading your app to the App Store and Play Store. And EAS Update enables over-the-air (OTA) updates, letting you push JavaScript and asset changes to your users instantly without going through the app store review process.
# Install EAS CLI
npm install -g eas-cli
# Login to your Expo account
eas login
# Configure EAS for your project
eas build:configure
# Build for iOS (production)
eas build --platform ios --profile production
# Build for Android (production)
eas build --platform android --profile production
# Build for both platforms simultaneously
eas build --platform all
# Submit to App Store & Play Store
eas submit --platform ios
eas submit --platform android
# Push an OTA update (no store review needed)
eas update --branch production --message "Fix login bug"⚖️ React Native vs Flutter: The Honest Comparison
The most common alternative to React Native is Flutter, Google's cross-platform framework using the Dart language. Flutter renders everything on its own canvas using the Skia/Impeller graphics engine — meaning pixel-perfect consistency across platforms, but at the cost of not using native UI components. React Native uses actual native components, so your app feels at home on each platform. If your team already knows JavaScript and React, React Native's learning curve is near zero. If pixel-perfect custom UI and maximum rendering control matter most, Flutter is compelling. For most product teams building conventional mobile apps, React Native's JavaScript ecosystem, faster iteration, and native feel make it the pragmatic choice.
🏢 Who Uses React Native in Production?
React Native isn't a framework for side projects — it's powering some of the world's most-used apps. Meta uses it for Facebook and parts of Instagram. Microsoft rebuilt the Xbox mobile app and Skype with it. Shopify's Shop app and large parts of their merchant-facing mobile experience run on React Native. Coinbase built their mobile trading app on it. Discord migrated core screens to React Native for faster development velocity. These aren't small experiments — they're flagship products serving millions of daily active users, proving that React Native is production-grade at scale.
🚀 Wrapping Up
React Native in 2025 is in its best shape ever. The New Architecture has addressed the performance criticisms that followed it for years. Expo has made the developer experience genuinely delightful. The ecosystem of libraries is mature, well-maintained, and increasingly powerful. If you already know React, you are closer to shipping a production iOS and Android app than you might think. The write-once-run-everywhere dream has never been more real — and React Native is leading the charge.
Responses (0)
No responses yet. Be the first to share your thoughts.