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.
Navigation architecture for scalable apps
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.



