Home About Services Work Blog Contact
connect@ajinthan.com · Colombo, Sri Lanka
Mobile

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.

A Cross-Platform Architecture for React Native Apps

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.

React NativeExpoMobileArchitecture
AT
Ajinthan Thavendrarajah

Based in Colombo, Sri Lanka, building scalable web & mobile products with .NET, NestJS, Next.js and Flutter.

Keep reading

Related articles.