DEV Community

amiryala
amiryala

Posted on

React Native Architecture Patterns That Scale (Part 3)

Architecture Patterns That Scale

Battle-tested patterns from applications serving millions of users


You've decided React Native fits your needs. Now the critical question: how do you architect an application that scales from MVP to enterprise without costly rewrites?

These aren't theoretical best practices — they're lessons learned from production deployments at scale.


The Foundation: Project Structure

Monorepo (Recommended for most teams)

/packages
  /mobile          # React Native app
  /web             # React web app (optional)
  /shared          # Shared business logic, types
  /ui              # Shared component library
/apps
  /ios             # iOS-specific native modules
  /android         # Android-specific native modules
Enter fullscreen mode Exit fullscreen mode

Why monorepo wins in 2026:

  • Expo's monorepo support is mature
  • Shared code between web and mobile is seamless
  • Single CI/CD pipeline for all platforms
  • Atomic commits across packages

Feature-Based Architecture

/src
  /features
    /auth
      /components
      /hooks
      /screens
      /services
      /store
      index.ts
    /profile
    /payments
  /shared
    /components
    /hooks
    /services
  /navigation
  /app
Enter fullscreen mode Exit fullscreen mode

Why this matters: Features are self-contained and testable. Teams can work independently. Clear ownership boundaries.


State Management in 2026

Tier 1: Server State (TanStack Query)

Use for: All data that lives on a server

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

export function useProducts(categoryId: string) {
  return useQuery({
    queryKey: ["products", categoryId],
    queryFn: () => productService.getByCategory(categoryId),
    staleTime: 5 * 60 * 1000,
  });
}

export function useCreateProduct() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: productService.create,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["products"] });
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Why TanStack Query dominates:

  • Automatic caching, refetching, synchronization
  • Optimistic updates out of the box
  • 90% of "state" in most apps is server state

Tier 2: Global Client State (Zustand)

Use for: App-wide client state (preferences, UI state, feature flags)

import { create } from "zustand";
import { persist } from "zustand/middleware";

interface AppState {
  theme: "light" | "dark" | "system";
  setTheme: (theme: AppState["theme"]) => void;
}

export const useAppStore = create<AppState>()(
  persist(
    (set) => ({
      theme: "system",
      setTheme: (theme) => set({ theme }),
    }),
    { name: "app-storage" }
  )
);
Enter fullscreen mode Exit fullscreen mode

Why Zustand over Redux in 2026:

  • Zero boilerplate
  • TypeScript-first
  • Built-in persistence
  • No providers needed

Tier 3: Local Component State

Just use useState and useReducer for UI state that doesn't need sharing.


Navigation Architecture

Expo Router (Recommended)

File-based routing, similar to Next.js:

/app
  _layout.tsx
  index.tsx
  /auth
    login.tsx
    register.tsx
  /(tabs)
    _layout.tsx
    profile.tsx
  /product
    [id].tsx
Enter fullscreen mode Exit fullscreen mode

Why Expo Router wins:

  • Deep linking works automatically
  • Type-safe routes
  • Familiar to web developers

API Layer Architecture

Service Pattern

// Base client with interceptors
export const apiClient = axios.create({
  baseURL: process.env.EXPO_PUBLIC_API_URL,
  timeout: 10000,
});

apiClient.interceptors.request.use((config) => {
  const token = useAuthStore.getState().accessToken;
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Feature service
export const productService = {
  getAll: async (): Promise<Product[]> => {
    const { data } = await apiClient.get("/products");
    return data;
  },

  create: async (dto: CreateProductDTO): Promise<Product> => {
    const { data } = await apiClient.post("/products", dto);
    return data;
  },
};
Enter fullscreen mode Exit fullscreen mode

Performance Patterns

Use FlashList for Large Lists

import { FlashList } from "@shopify/flash-list";

<FlashList
  data={products}
  renderItem={({ item }) => <ProductCard product={item} />}
  estimatedItemSize={120}
/>
Enter fullscreen mode Exit fullscreen mode

Image Optimization with expo-image

import { Image } from "expo-image";

<Image
  source={{ uri: product.imageUrl }}
  contentFit="cover"
  placeholder={blurhash}
  transition={200}
/>
Enter fullscreen mode Exit fullscreen mode

Memoization Strategy

// Memoize expensive computations
const sortedProducts = useMemo(
  () => products.sort((a, b) => b.rating - a.rating),
  [products]
);

// Memoize callbacks
const handlePress = useCallback((id: string) => {
  navigation.navigate("ProductDetail", { productId: id });
}, [navigation]);
Enter fullscreen mode Exit fullscreen mode

Testing Architecture

The Testing Pyramid

        /\
       / E2E \      10% (Detox/Maestro)
      /________\
     / Integration \ 30% (RNTL)
    /______________\
   /      Unit       \ 60% (Jest)
  /____________________\
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Start with the right structure — Monorepo + feature-based architecture
  2. Layer your state — Server (TanStack Query) → Global (Zustand) → Local (useState)
  3. Type everything — TypeScript catches bugs before runtime
  4. Optimize later — Profile first, don't premature optimize
  5. Test the pyramid — More unit tests, fewer E2E tests

Next in series: Team Composition & Hiring Strategies


About the author: Mobile architect with 10+ years building applications at scale. Currently at Lotus Innovations.

Top comments (0)