Skip to main content

Command Palette

Search for a command to run...

Expo Router Architecture for Large Apps: Feeds, Realtime, Offline, Scale

Updated
8 min read
Expo Router Architecture for Large Apps: Feeds, Realtime, Offline, Scale

Modern apps are not just a pile of screens. They are a living system of features, data flows, offline behavior, and performance constraints. If you have only shipped small apps, the jump to something Instagram or Uber sized can feel messy fast.

This guide is about architecture, not cloning UI. We will look at how large apps are structured and how Expo Router helps keep a React Native codebase sane as it grows.


Why simple folder structures break at scale

A flat list of screens works for a prototype. It fails when teams are split by features, multiple flows run in parallel, and different parts of the app ship at different speeds. The pain points usually look like this:

  • Navigation logic gets scattered across files

  • Shared state becomes a global dumping ground

  • Feature ownership becomes unclear

  • Refactors become risky

Large apps need an architecture that maps to how teams think and ship.


Why architecture matters in React Native

React Native lets you move fast, but scale introduces friction:

  • Startup time grows as bundles get bigger

  • Navigation trees become hard to reason about

  • Async data flows become tangled

  • Realtime features need careful isolation

Architecture is not about making things complex. It is about creating boundaries that reduce complexity.


A production-grade Expo Router folder architecture

Expo Router uses file based routing. That makes it easy to build a route map that reflects your product structure.

Here is a practical, feature focused structure:

app/
  _layout.tsx
  (auth)/
    login.tsx
    otp.tsx
  (tabs)/
    _layout.tsx
    feed/
      index.tsx
      [postId].tsx
    messages/
      index.tsx
      [threadId].tsx
    rides/
      index.tsx
      [rideId].tsx
    downloads/
      index.tsx
  modal/
    report.tsx
features/
  feed/
    api.ts
    hooks.ts
    components/
  messaging/
    api.ts
    realtime.ts
    store.ts
  rides/
    api.ts
    tracking.ts
  media/
    uploader.ts
  downloads/
    cache.ts
shared/
  ui/
  hooks/
  store/
  api/

The app/ folder is your navigation map. The features/ folder is how you keep logic organized. The shared/ folder is for cross cutting pieces.


Feature based separation in large apps

Feature based structure mirrors how product teams work. Instagram, WhatsApp, Uber, and Netflix each have features that evolve independently.

  • Instagram: feed, stories, search, reels, messaging

  • WhatsApp: chats, calls, status, media

  • Uber: rides, driver location, payments, support

  • Netflix: home, player, downloads, profiles

Each feature has its own API, state, and UI. That isolation keeps changes contained and reduces merge conflicts.


With Expo Router, the file system defines your routes. The navigation hierarchy is readable at a glance.

Use route groups and shared layouts to keep flows clean:

// app/_layout.tsx
import { Stack } from "expo-router";

export default function RootLayout() {
  return <Stack screenOptions={{ headerShown: false }} />;
}
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";

export default function TabsLayout() {
  return (
    <Tabs>
      <Tabs.Screen name="feed" />
      <Tabs.Screen name="messages" />
      <Tabs.Screen name="rides" />
      <Tabs.Screen name="downloads" />
    </Tabs>
  );
}

This keeps navigation declarations close to the structure users experience.


Authentication flow architecture

Large apps use protected routes. You can group auth routes and gate the main app at the root layout.

// app/_layout.tsx
import { Stack } from "expo-router";
import { useAuth } from "../shared/store/auth";

export default function RootLayout() {
  const { isSignedIn, isReady } = useAuth();

  if (!isReady) return null;

  return (
    <Stack screenOptions={{ headerShown: false }}>
      {!isSignedIn ? (
        <Stack.Screen name="(auth)" />
      ) : (
        <Stack.Screen name="(tabs)" />
      )}
    </Stack>
  );
}

This keeps auth logic centralized and avoids repeating checks on every screen.


State management strategies for large apps

You do not need one global store for everything. A common pattern is to split state by scope:

  • Local component state for UI

  • Feature stores for business logic

  • Global session state for auth and app level config

A lightweight combination like Zustand or Redux Toolkit, plus React Query for server state, works well. The key idea is to keep server state and client state separate.


API handling and networking layers

Large apps use an API layer that is shared across features but not mixed into UI.

// shared/api/client.ts
import axios from "axios";

export const api = axios.create({
  baseURL: "https://api.example.com",
  timeout: 10000,
});
// features/feed/api.ts
import { api } from "../../shared/api/client";

export async function fetchFeed(page: number) {
  const res = await api.get("/feed", { params: { page } });
  return res.data;
}

This makes testing easier and keeps network concerns out of components.


Realtime systems at scale

Realtime is a big reason these apps feel alive. Each app has a different flavor:

  • Instagram and Netflix: live updates for feed and recommendations

  • WhatsApp: messaging and presence

  • Uber: location updates and ride state

The common pattern is a realtime layer per feature, not a single global listener. That keeps traffic and state changes contained.

// features/messaging/realtime.ts
import { createClient } from "./socket";

export function connectToThread(threadId: string, onMessage: (m: any) => void) {
  const socket = createClient();
  socket.emit("join", { threadId });
  socket.on("message", onMessage);
  return () => socket.disconnect();
}

Offline first support and caching

Offline behavior is not only for flights. It improves perceived performance for everyone.

  • Cache feed data and media metadata

  • Queue actions like likes, sends, or ride updates

  • Sync when network returns

React Query or a custom cache with persistence can power this. Keep offline logic feature scoped so it does not pollute unrelated code.


App startup optimization techniques

Large apps fail at startup when too much work happens at once. Common fixes include:

  • Load only the session state needed for the first screen

  • Lazy load heavy features

  • Defer analytics and logging until after first render

  • Preload only what the next screen needs

Expo Router helps by keeping routes lazy and allowing nested layouts to load incrementally.


Performance considerations in production apps

Performance is not one thing. It is a mix of frame rate, memory use, and network behavior.

Best practices include:

  • Keep render trees small on high traffic screens

  • Use FlatList properly and avoid heavy inline functions

  • Reduce unnecessary rerenders with memoization

  • Avoid giant screens that do too much


Shared layouts and nested routing in Expo Router

Shared layouts give you control without duplication. A player layout in Netflix or a feed layout in Instagram can be shared without repeating logic.

// app/(tabs)/feed/_layout.tsx
import { Stack } from "expo-router";

export default function FeedLayout() {
  return <Stack />;
}

Nested routing makes deep navigation predictable and keeps URL like paths clean.


How these apps map to Expo Router thinking

Here is how the feature architecture maps to well known apps:

  • Instagram: feed, stories, reels, messaging as separate feature modules

  • WhatsApp: chat threads with a realtime layer per conversation

  • Uber: rides feature with tracking and live updates

  • Netflix: heavy content delivery with offline downloads

The apps are different, but the architectural patterns are similar.


Tradeoffs teams make at scale

There is no perfect architecture. Teams trade simplicity for scale:

  • More folders for clarity, but more onboarding time

  • More layers for stability, but more boilerplate

  • Feature isolation, but more integration testing

The best structure is the one that your team can maintain as the product grows.


Diagram ideas you can reuse

Auth flow

  +-------------------+
  | App Root Layout   |
  +-------------------+
             |
             v
       +-------------+
       | Signed In?  |
       +-------------+
         |         |
        No        Yes
         |         |
         v         v
  +---------------+   +----------------+
  | (auth) routes |   | (tabs) routes  |
  +---------------+   +----------------+
                          |   |   |   |
                          v   v   v   v
                       +-----+ +---------+ +-----+ +----------+
                       |feed| |messages | |rides| |downloads|
                       +-----+ +---------+ +-----+ +----------+
Feature structure

  +-----------+
  | features/ |
  +-----------+
     |   |   |   |    |
     v   v   v   v    v
   +----+ +---------+ +-----+ +-----+ +----------+
   |feed| |messaging| |rides| |media| |downloads |
   +----+ +---------+ +-----+ +-----+ +----------+
     |
     v
  +-------------------------+
  | api + hooks + components|
  +-------------------------+

Common mistakes to avoid

  • Treating the router folder as a dumping ground

  • Putting all state in one global store

  • Letting API logic live inside UI components

  • Loading all feature code at startup

  • Mixing realtime logic across unrelated features


Short conclusion

Large scale apps are built on clear boundaries. Expo Router gives you a clean navigation map, but the real win is pairing it with feature based architecture. Start simple, keep features isolated, and grow the structure with your product.