React Native’s New Architecture is now default. Expo is the official recommendation. If you already ship React on the web, you can reuse most of your mental model and a surprising amount of your code.
Here are 6 production patterns that let a Next.js developer ship real mobile apps fast.
1. Replace React Router With File-Based Routing Using Expo Router
If you know Next.js App Router, you already know Expo Router. The file system defines navigation structure. Layout files wrap screens.
Before (Next.js App Router)
// app/jobs/[id]/page.tsx
import { useParams } from "next/navigation";
import { useJob } from "@/shared/hooks/useJob";
export default function JobPage() {
const { id } = useParams<{ id: string }>();
const { data, isLoading } = useJob(id);
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{data.title}</h1>
<p>{data.company}</p>
</div>
);
}
After (Expo Router)
// app/jobs/[id].tsx
import { useLocalSearchParams } from "expo-router";
import { useJob } from "@shared/hooks/useJob";
import { View, Text } from "react-native";
export default function JobScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { data, isLoading } = useJob(id);
if (isLoading) return <Text>Loading...</Text>;
return (
<View>
<Text>{data.title}</Text>
<Text>{data.company}</Text>
</View>
);
}
The routing mental model is identical. Dynamic segments, nested layouts, and shared data hooks all transfer directly. Most web developers are productive in hours, not weeks.
2. Share 70% of Business Logic in a Monorepo
The salary premium comes from code reuse. Not from knowing how to center a button on iOS.
Shared package
// packages/shared/src/api/jobs.ts
import { z } from "zod";
export const JobSchema = z.object({
id: z.string(),
title: z.string(),
company: z.string(),
salary: z.string(),
});
export type Job = z.infer<typeof JobSchema>;
export async function fetchJobs(): Promise<Job[]> {
const res = await fetch("https://api.example.com/jobs");
const json = await res.json();
return json.map((j: unknown) => JobSchema.parse(j));
}
Used in Next.js
import { useQuery } from "@tanstack/react-query";
import { fetchJobs } from "@shared/api/jobs";
export function useJobs() {
return useQuery({
queryKey: ["jobs"],
queryFn: fetchJobs,
});
}
Used in Expo
import { useQuery } from "@tanstack/react-query";
import { fetchJobs } from "@shared/api/jobs";
export function useJobs() {
return useQuery({
queryKey: ["jobs"],
queryFn: fetchJobs,
});
}
API client, Zod validation, TypeScript types, React Query hooks, and Zustand stores are identical. Only the UI layer changes. In well structured codebases, 60 to 80% is shared.
If you care about structuring that shared layer correctly, the patterns map directly to what I outlined in the JavaScript application architecture system design guide.
3. Replace div + CSS With View + StyleSheet and Flexbox Only
There is no CSS cascade in React Native. Everything is Flexbox and inline style objects.
Before (Web)
<div className="card">
<h2 className="title">{job.title}</h2>
<p className="company">{job.company}</p>
</div>
.card {
display: flex;
flex-direction: column;
padding: 16px;
}
.title {
font-size: 20px;
font-weight: 600;
}
After (React Native)
import { View, Text, StyleSheet } from "react-native";
export function JobCard({ job }: { job: Job }) {
return (
<View style={styles.card}>
<Text style={styles.title}>{job.title}</Text>
<Text style={styles.company}>{job.company}</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
flexDirection: "column",
padding: 16,
},
title: {
fontSize: 20,
fontWeight: "600",
},
company: {
fontSize: 16,
},
});
No CSS grid. No media queries. No cascade. Just Flexbox. The constraint simplifies architecture and reduces layout bugs across platforms.
4. Optimize Lists With FlatList Instead of map
On the web you can get away with mapping 1,000 elements. On mobile you cannot.
Before (Web)
{jobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
After (React Native)
import { FlatList } from "react-native";
<FlatList
data={jobs}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <JobCard job={item} />}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews
/>
FlatList virtualizes rows. It renders only what is visible. Combined with React.memo and useCallback, this is the difference between smooth 60fps scroll and users uninstalling your app.
5. Store Auth Tokens in SecureStore, Not AsyncStorage
On web you use httpOnly cookies. On mobile you must use hardware backed storage.
Before (Insecure pattern)
import AsyncStorage from "@react-native-async-storage/async-storage";
await AsyncStorage.setItem("token", token);
After (Correct pattern)
import * as SecureStore from "expo-secure-store";
const TOKEN_KEY = "auth_token";
export async function setToken(token: string) {
await SecureStore.setItemAsync(TOKEN_KEY, token);
}
export async function getToken() {
return SecureStore.getItemAsync(TOKEN_KEY);
}
SecureStore uses iOS Keychain and Android Keystore. AsyncStorage stores plain text. The difference is equivalent to cookies versus localStorage on the web.
6. Ship JS Updates With EAS Update Instead of Waiting for App Store Review
Web developers are used to instant deploys. Expo gives you something close.
Before (Traditional mobile flow)
Build new binary.
Upload to App Store.
Wait for review.
Users update manually.
After (Expo OTA update)
eas update --branch production --message "Fix crash on job detail"
JavaScript changes are delivered over the air. Users get fixes on next app launch. Native changes still require a store submission, but 90% of day to day bug fixes do not.
For teams used to CI driven web deploys, this is a massive productivity gain.
If you already ship React apps, React Native with Expo is not a new ecosystem. It is a new render target. The component model, hooks, state management, and TypeScript patterns all transfer.
Install Expo. Take an existing Next.js project. Rebuild one feature end to end. In 30 days you go from web only to web plus mobile. That combination is still rare, and rare skills get paid.
Top comments (0)