6 React Native + Expo Patterns That Let Web Developers Ship iOS and Android Apps in 30 Days
Source: Dev.to
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();
const { data, isLoading } = useJob(id);
if (isLoading) return Loading...;
return (
## {data.title}
{data.company}
);
}
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();
const { data, isLoading } = useJob(id);
if (isLoading) return Loading...;
return (
{data.title}
{data.company}
);
}
The routing mental model is identical. Dynamic segments, nested layouts, and shared data hooks all transfer directly. Most web developers become 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;
export async function fetchJobs(): Promise {
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 %–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)
## {job.title}
{job.company}
.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 (
{job.title}
{job.company}
);
}
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) => (
))}
After (React Native)
import { FlatList } from "react-native";
item.id}
renderItem={({ 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 60 fps scroll and users uninstalling your app.
5. Store Auth Tokens in SecureStore, Not AsyncStorage
On the 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";
(Continue with your SecureStore implementation here.)
6. (Placeholder for the sixth pattern)
(Add your sixth production pattern here, preserving the same structure as the previous items.)
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 the 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’s 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 can go from web‑only to web + mobile. That combination is still rare, and rare skills get paid.