After 90 days of building with React and Next.js, moving to mobile feels like switching from a familiar framework to an entirely different paradigm. React Native for React developers isn't a straight port—it's a mindset shift. You'll recognize the component and hook patterns immediately, but the absence of a DOM, browser APIs, and CSS-in-the-browser changes everything about how you structure layout, styling, and performance.
What transfers — Components, hooks, state management and TypeScript
The good news: if you've shipped React applications, you already understand the core mental model of React Native. Components remain functional, hooks work the same way, and your state management patterns carry over directly.
Let's take a practical example. A form component you'd build for web:
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async () => {
setIsLoading(true);
try {
await loginUser(email, password);
// Navigate to home
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input value={password} onChange={(e) => setPassword(e.target.value)} type="password" />
<button disabled={isLoading}>{isLoading ? 'Signing in...' : 'Sign in'}</button>
</form>
);
}
In React Native, the logic structure is identical:
import { View, TextInput, TouchableOpacity, Text, ActivityIndicator } from 'react-native';
export function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async () => {
setIsLoading(true);
try {
await loginUser(email, password);
// Navigate to home
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};
return (
<View>
<TextInput value={email} onChangeText={setEmail} placeholder="Email" />
<TextInput value={password} onChangeText={setPassword} placeholder="Password" secureTextEntry />
<TouchableOpacity onPress={handleSubmit} disabled={isLoading}>
{isLoading ? <ActivityIndicator /> : <Text>Sign in</Text>}
</TouchableOpacity>
</View>
);
}
The hooks are unchanged. useState, useEffect, useContext, useCallback—all work identically. If you use Redux, Zustand, or Jotai on web, those libraries have React Native support or work without changes.
TypeScript support is excellent and recommended from day one. The type safety you've built on the web side applies equally here, and catching errors at compile time matters more on mobile where testing cycles are longer.
What is different — No DOM or CSS. Flexbox everywhere, StyleSheet for styles
React Native has no DOM. There's no HTML, no CSS cascade, no browser APIs. Instead, components render to native platform primitives—UIKit on iOS, View hierarchy on Android. This means no <div>, <span>, or semantic HTML. Every layout is built from View, Text, Image, and component-specific elements.
Layout itself is always Flexbox. There's no Grid, no absolute positioning (well, there is, but it's uncommon), no `display: block` or `display: inline`. Every container is a flex container by default, with sensible defaults: flexDirection: 'column' and flex: 0.
Styling lives in JavaScript objects, not CSS files. You'll use StyleSheet.create() for performance:
import { StyleSheet, View, Text } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 16,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: '600',
color: '#333',
marginBottom: 8,
},
subtitle: {
fontSize: 14,
color: '#666',
textAlign: 'center',
},
});
export function HomeScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome</Text>
<Text style={styles.subtitle}>This is mobile layout</Text>
</View>
);
}
StyleSheet.create() validates your styles at runtime and optimizes them under the hood. No media queries—instead, you use Dimensions API or libraries like react-native-responsive-screen to adapt layouts for different screen sizes.
There's no inheritance of font styles from parent elements. Every <Text> component needs explicit styling. There are no global CSS resets. This feels repetitive at first, but it forces intentionality about design systems. Build a component library early: buttons, cards, input fields, typography. Avoid styling scattered across 50 files.
Navigation — React Navigation or Expo Router instead of browser routing
Browser routing and query parameters are gone. React Navigation (the de facto standard) uses a stack-based navigation model closer to how native apps actually work. You push and pop screens off a navigation stack, manage tabs, and handle deep linking differently.
Here's a basic example with a native stack navigator:
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { HomeScreen } from './screens/HomeScreen';
import { DetailScreen } from './screens/DetailScreen';
const Stack = createNativeStackNavigator();
export function RootNavigator() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Detail" component={DetailScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
Inside a screen, you navigate like this:
import { useNavigation } from '@react-navigation/native';
export function HomeScreen() {
const navigation = useNavigation();
return (
<TouchableOpacity onPress={() => navigation.navigate('Detail', { id: 123 })}>
<Text>Go to detail</Text>
</TouchableOpacity>
);
}
Routing params are passed as a second argument. Accessing them in the target screen:
export function DetailScreen({ route }) {
const { id } = route.params;
// Use id
}
Expo Router is newer and brings file-based routing similar to Next.js, which might feel more familiar. Either works; choose based on your team's preference and the ecosystem you're working in (Expo vs bare React Native).
Performance — Long lists need FlashList. Animations belong on the UI thread
React Native's FlatList works for moderate lists. But scroll through a 1,000-item list and you'll see frame drops. This is where FlashList (from Shopify) becomes essential. It's a drop-in replacement that virtualizes rendering far more aggressively:
import { FlashList } from '@shopify/flash-list';
export function ItemListScreen() {
const items = Array.from({ length: 10000 }, (_, i) => ({ id: i, title: `Item ${i}` }));
return (
<FlashList
data={items}
renderItem={({ item }) => <ItemRow item={item} />}
keyExtractor={(item) => item.id.toString()}
estimatedItemSize={60}
/>
);
}
The estimatedItemSize prop is crucial—it helps FlashList estimate scroll position. Get this wrong and scrolling feels janky.
Animations are another gotcha. JavaScript-driven animations freeze when the main thread is busy. Use React Native's built-in Animated API or Reanimated for smooth, UI-thread animations:
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
export function AnimatedBox() {
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: offset.value }],
};
});
const gesture = Gesture.Pan().onChange((e) => {
offset.value = e.translationX;
}).onEnd(() => {
offset.value = withSpring(0);
});
return (
<GestureDetector gesture={gesture}>
<Animated.View style={[{ width: 100, height: 100, backgroundColor: 'blue' }, animatedStyle]} />
</GestureDetector>
);
}
Animations declared with Animated or Reanimated bypass JavaScript and run on the native UI thread, staying smooth even under load.
Common mistakes
- Forgetting that Text must always be wrapped in <Text> components. You cannot do
<View>Hello</View>. Raw strings render as errors. - Assuming CSS knowledge transfers directly. No cascading styles, no media queries (use Dimensions or responsive libraries), no global resets. Your mental model needs to shift.
- Using FlatList for large lists without scrolling optimization. Switch to FlashList early. Your future self will thank you when you're debugging frame drops at 2 AM.
- Running computationally expensive work on the main thread during animations. Use
InteractionManager.runAfterInteractions()or move work off-thread with background tasks. - Not testing on real devices. Simulator performance is misleading. A smooth scroll on macOS may stutter on a mid-range Android phone.
Key takeaways
- Component architecture, hooks, and state management patterns from React transfer directly. TypeScript is your friend.
- There's no DOM or CSS. Learn Flexbox deeply—it's your primary layout tool. Style with JavaScript objects and
StyleSheet.create(). - Navigation is stack-based, not URL-based. Use React Navigation or Expo Router; deep linking works but requires explicit setup.
- Performance requires intentionality: FlashList for long lists,
AnimatedorReanimatedfor smooth motion, and always profile on real hardware. - Build a component library early. Intentional design systems matter more on mobile than the web.






Comments
Loading comments…