Skip to main content

Command Palette

Search for a command to run...

How Instagram, WhatsApp, Uber & Netflix Would Be Built Today Using Expo Router

Updated
4 min read

Building a todo app is easy. Building Instagram is hard.

The difference isn't the code. It's the architecture.

Let's look at how massive mobile apps scale using React Native and Expo Router. No fluff. Just engineering.

Why Simple Folders Fail at Scale

Beginners group files by type.

  • /screens

  • /components

  • /hooks

This works for 10 screens. It breaks at 100.

When fixing a bug in the "Chat" feature, you jump between five different folders. Context switching kills productivity.

Production thinking groups by feature.

Feature-Based Folder Architecture

Big apps isolate features. A feature owns its screens, components, and logic.

/src
  /app               # Expo Router (file-based routing)
    /(auth)          # Auth group (login, signup)
    /(tabs)          # Main app tabs
  /features
    /feed            # Instagram feed logic
    /chat            # WhatsApp messaging logic
    /map             # Uber map logic
    /video           # Netflix player logic
  /core
    /api             # Network layer
    /storage         # Local DB, async storage
    /ui              # Dumb, reusable components (Button, Text)

Expo Router handles the URLs. The features folder handles the business logic.

Scaling the Giants

Let's break down how specific apps solve hard problems.

Instagram: Feeds & Shared Layouts

The Challenge: Endless scrolling, heavy media, nested navigation.

The Fix: Shared layouts and nested routing. Expo Router lets you wrap routes in layouts. The bottom tab bar stays mounted while you navigate deep into a profile.

  • Media Caching: Don't re-download images. Use expo-image with aggressive disk caching.

  • List Virtualization: Never use ScrollView for feeds. Use FlashList. It only renders what's on screen.

WhatsApp: Realtime & Offline-First

The Challenge: Messages must work without internet and sync instantly when back online.

The Fix: Local-first architecture.

  1. User sends message.

  2. App saves to local database (WatermelonDB or SQLite) immediately. UI updates instantly.

  3. Background worker pushes to server via WebSockets.

  4. Server confirms. UI updates status to "Delivered".

If offline, messages queue locally. Network returns? Sync automatically.

Uber: Maps & Live Location

The Challenge: Maps eat RAM. Live tracking drains battery. UI must stay smooth at 60fps.

The Fix: Isolate heavy native code.

  • Throttling: Don't update React state on every GPS ping. Throttle updates to 1 per second.

  • Native Modules: Run complex routing math on the native thread (Swift/Kotlin), not the JavaScript thread.

  • Map Clustering: Group 1,000 nearby cars into 1 icon.

Netflix: Heavy Content & Startup

The Challenge: App must open in < 1 second. Video players are heavy.

The Fix: Startup optimization.

  • Defer Loading: Don't load the video player code on app launch. Load it only when the user clicks "Play".

  • Prefetching: Fetch the first row of movie posters while the splash screen is visible.

  • Skeleton UI: Show gray boxes immediately. Fill with data later. Perceived speed > actual speed.

Core Systems at Scale

Authentication Flow

Don't check auth on every screen. Do it at the root.

// app/_layout.tsx
export default function RootLayout() {
  const { user, isLoading } = useAuth();

  if (isLoading) return <SplashScreen />;

  // Expo Router handles the redirect automatically
  return user ? <SignedInStack /> : <SignedOutStack />;
}

API & State Management

Stop putting server data in Redux or Zustand.

  • Server State: Use React Query (TanStack Query). It handles caching, retries, and background refetching.

  • Client State: Use Zustand. Only for UI state (is modal open? which tab is active?).

[Component] --> [React Query] --> [API Wrapper] --> [Server]
     | 
     +---> [Zustand Store] (UI State only)

App Startup Lifecycle

Every millisecond counts.

  1. Native Init: iOS/Android boots the app.

  2. JS Bundle: Metro loads the JS. (Keep bundle small!)

  3. Hydration: React mounts the root component.

  4. Data Fetch: API calls fire.

Optimization: Use Hermes engine. Enable RAM bundles (inline requires). Delay non-critical initializations (like analytics) until after the first paint.

Tradeoffs at Scale

There is no perfect architecture. Only tradeoffs.

  • Local-first (WhatsApp): Great UX. Hard to write. Sync conflicts are painful.

  • Modularization: Speeds up build times. Makes sharing code between features annoying.

  • Expo Router: Amazing developer experience. Sometimes limits ultra-custom native navigation transitions.

Teams pick the poison that hurts the least for their specific product.

Quick Takeaway

  • Stop grouping by file type. Group by feature.

  • Use Expo Router layouts. Keep tabs and headers mounted.

  • Separate server state and client state. React Query for API, Zustand for UI.

  • Think offline-first. The network will fail. Plan for it.

  • Optimize the first second. Defer heavy code. Fake the speed with skeleton screens.

Build for the app you want to have in two years, not the app you have today.