A Cross-Platform Architecture for React Native Apps
How I structure React Native projects so iOS and Android share almost everything — navigation, data, and a clean line between platform-specific and shared code.

The promise of React Native is one codebase, two platforms. The reality depends entirely on how you draw the lines inside that codebase. Here's the structure I've used to ship real cross-platform products without the two builds drifting apart.
Expo or bare? Decide once, deliberately
- Expo (managed) — start here for most apps. Over-the-air updates, a huge module library, and far less native toolchain pain.
- Bare workflow — only when you need a native capability Expo doesn't expose, or a custom native module. You can always eject later; you rarely need to.
For 80% of products, Expo plus a few config plugins covers everything.
Layer the codebase
I keep a strict separation between layers so each can change independently:
src/
app/ # screens & navigation
features/ # domain modules (auth, booking, profile)
components/ # shared, presentational UI
services/ # API clients, storage, push
hooks/ # reusable logic
theme/ # tokens, spacing, typography
Screens compose features. Features use services and hooks. UI components never call the network. When this boundary holds, you can refactor a feature without touching navigation, and swap an API without touching the UI.
Share the data layer ruthlessly
Networking, caching and models are 100% shared between platforms. Wrap your API in typed service functions and feed them through a cache like React Query:
export function useBooking(id: string) {
return useQuery({
queryKey: ["booking", id],
queryFn: () => api.getBooking(id),
});
}
Screens stay declarative; retries, loading and caching live in one place.
Isolate the platform-specific 5%
Some things genuinely differ — permissions, haptics, the occasional native UI affordance. Don't scatter Platform.OS checks through your screens. Push them behind a single module:
// services/haptics.ts
export const haptics = Platform.select({
ios: iosHaptics,
android: androidHaptics,
})!;
The goal isn't zero platform-specific code. It's platform-specific code that lives in known, named places instead of leaking everywhere.
Design system before screens
Define spacing, colors and typography as tokens on day one. Build a small set of primitives — Text, Button, Card — on top of them. Every screen you build afterwards is faster and automatically consistent across both platforms.
Get the layers right and "cross-platform" stops being a feature you fight for and becomes the default the architecture gives you for free.
Keep reading


