react-native
React Native brings React's declarative component model to iOS and Android: you write components in JavaScript or TypeScript, and they render as real native UI controls, not a webview. You get React's state and props model, live-reloading JavaScript during development, and full access to native platform APIs when you need to drop down. Apps can target iOS 15.1+ and Android 7.0 (API 24)+, and the same code can reach other platforms through out-of-tree renderers.
The proven way for React teams to ship both app stores from one codebase, backed by Meta and a large contributor ecosystem. Budget real time for native tooling and version upgrades, and start with Expo unless you have a specific reason not to.
Use it if
- Your team already knows React and you need one codebase shipping real native UI on both iOS and Android
- You want fast iteration: JavaScript changes reload in seconds without rebuilding the native app
- You need access to native capabilities through an ecosystem of existing native modules, with the option to write your own
- You are building a mostly standard app (lists, forms, navigation, media) where native look and feel matters more than pixel-identical custom rendering
- Nobody on the team can debug Xcode or Gradle: native build failures are a fact of life and the JavaScript layer will not shield you from them
- Your app is dominated by heavy custom rendering, 3D, or per-frame animation work, where a fully native app or a game engine fits better
- You cannot budget for upgrades: React Native is still on 0.x with frequent releases, and moving between versions regularly breaks native dependencies
- Your product is already a working responsive web app: wrapping it with Capacitor is far less work than rebuilding the UI in React Native
- You need to build iOS locally without a Mac: iOS builds require macOS unless you lean on Expo's cloud services
Setup reality
The docs steer new apps toward a framework, in practice Expo, which hides most of the native setup until you need a custom native module. Going bare means installing Xcode, Android Studio, JDK, SDK paths, and CocoaPods, and each of those can fail independently on a fresh machine. The package pins peer dependencies tightly (0.86 wants react ^19.2.3), so version mismatches surface as confusing install or runtime errors. The New Architecture is the default now, and older native libraries that never migrated may not work. Upgrades are their own project: the community upgrade-helper diff is the honest tool for the job.
Patterns
Start a new appcreate-app
# recommended: Expo framework
npx create-expo-app@latest MyApp
cd MyApp && npx expo start
# bare React Native (community CLI)
npx @react-native-community/cli init MyAppThe bare path needs Xcode and Android Studio configured before the first run; Expo defers that until you eject into custom native code.
Build a basic screen with core componentsbasic-screen
import { StyleSheet, Text, View } from 'react-native';
export default function Hello() {
return (
<View style={styles.container}>
<Text style={styles.title}>Hello</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
title: { fontSize: 24, fontWeight: '600' },
});There is no HTML here: text must live inside <Text>, and layout is flexbox with flexDirection defaulting to column, not row.
Render a long list efficientlyrender-list
import { FlatList, Text } from 'react-native';
function Users({ users }) {
return (
<FlatList
data={users}
keyExtractor={(item) => String(item.id)}
renderItem={({ item }) => <Text>{item.name}</Text>}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
/>
);
}Mapping an array inside a ScrollView renders every row at once; FlatList virtualizes and is the correct tool past a few dozen items.
Fetch data on mountfetch-data
import { useEffect, useState } from 'react';
function useStats() {
const [stats, setStats] = useState(null);
useEffect(() => {
let alive = true;
fetch('https://api.example.com/stats')
.then((r) => r.json())
.then((data) => { if (alive) setStats(data); })
.catch(console.error);
return () => { alive = false; };
}, []);
return stats;
}fetch is built in, but Android emulators reach your host machine at 10.0.2.2, not localhost.
Handle taps with Pressablehandle-press
import { Pressable, Text } from 'react-native';
<Pressable
onPress={submit}
style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
hitSlop={8}
>
<Text>Submit</Text>
</Pressable>Pressable replaced the older Touchable* components as the recommended API; hitSlop enlarges the touch target without changing layout.
Controlled text inputtext-input
import { useState } from 'react';
import { TextInput } from 'react-native';
function EmailField() {
const [email, setEmail] = useState('');
return (
<TextInput
value={email}
onChangeText={setEmail}
placeholder="you@example.com"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
);
}autoCapitalize defaults to sentences, which silently uppercases the first letter of emails and usernames on iOS.
Branch code per platformplatform-specific
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
header: {
paddingTop: Platform.select({ ios: 44, android: 24 }),
...Platform.select({
ios: { shadowOpacity: 0.2 },
android: { elevation: 4 },
}),
},
});For whole components, Button.ios.js and Button.android.js next to each other resolve automatically from import './Button'.
Display a remote imageshow-image
import { Image } from 'react-native';
<Image
source={{ uri: 'https://example.com/avatar.png' }}
style={{ width: 64, height: 64, borderRadius: 32 }}
resizeMode="cover"
/>Remote images render at zero size unless you give explicit width and height; only bundled images infer their dimensions.
Navigate between screensnavigate-screens
// npm i @react-navigation/native @react-navigation/native-stack
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Details" component={Details} />
</Stack.Navigator>
</NavigationContainer>
);
}Navigation is not part of core; react-navigation also needs react-native-screens and react-native-safe-area-context installed.
Respect the system color schemedark-mode
import { useColorScheme, View, Text } from 'react-native';
function Card() {
const scheme = useColorScheme();
const dark = scheme === 'dark';
return (
<View style={{ backgroundColor: dark ? '#111' : '#fff' }}>
<Text style={{ color: dark ? '#fff' : '#111' }}>Hi</Text>
</View>
);
}The hook re-renders on system theme changes; hardcoded colors elsewhere in the tree will not follow along.
Open links and other appsopen-url
import { Linking, Alert } from 'react-native';
async function openDocs() {
const url = 'https://reactnative.dev';
const ok = await Linking.canOpenURL(url);
if (ok) await Linking.openURL(url);
else Alert.alert('Cannot open URL');
}On iOS, canOpenURL for custom schemes (tel:, whatsapp:) returns false unless the scheme is listed in LSApplicationQueriesSchemes.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| expo | npm | Almost always for new apps: it is the recommended framework layer over React Native and removes most native tooling pain. |
| @capacitor/core | npm | You already have a web app and want it in the stores as a webview with native plugin access. |
| nativescript | npm | You want direct JavaScript access to native APIs without the React model, with Angular or Vue flavors available. |