ARCHITECTUREJan 06, 2026 // 11 min read // Written by Founders

MASTERING EXPO ROUTER: FILE-BASED NAVIGATION FOR IOS AND ANDROID

Expo Router fundamentally changes how React Native developers think about navigation. Instead of manually wiring up React Navigation stacks, tabs, and drawers with imperative configuration objects, you define routes by creating files in your /app directory — the same convention that made Next.js routing intuitive for web developers.

The result is a navigation system that supports deep linking out of the box, enables type-safe route parameters, and works identically on iOS, Android, and web.

File-Based Navigation Conventions

Expo Router automatically creates navigation routes based on your /app directory structure. The mapping is predictable:

  • app/index.tsx → Home screen (/)
  • app/settings.tsx → Settings screen (/settings)
  • app/details/[id].tsx → Dynamic detail screen (/details/142)
  • app/(tabs)/_layout.tsx → Tab navigator wrapping child routes
  • app/+not-found.tsx → 404 fallback screen

Directory Structure Example

app/
├── _layout.tsx          # Root Stack navigator
├── index.tsx            # Home screen (/)
├── (tabs)/
│   ├── _layout.tsx      # Tab navigator
│   ├── index.tsx        # Feed tab
│   ├── search.tsx       # Search tab
│   └── profile.tsx      # Profile tab
├── details/
│   └── [id].tsx         # Dynamic route (/details/:id)
├── modal.tsx            # Modal screen
└── +not-found.tsx       # 404 handler

Configuring Deep Links

Expo Router includes built-in deep linking support. Universal links on iOS and App Links on Android work automatically when you configure your app.json:

{
  "expo": {
    "scheme": "venelxapp",
    "web": {
      "bundler": "metro"
    },
    "ios": {
      "associatedDomains": ["applinks:app.venelx.com"]
    },
    "android": {
      "intentFilters": [{
        "action": "VIEW",
        "data": [{ "scheme": "https", "host": "app.venelx.com" }],
        "category": ["BROWSABLE", "DEFAULT"]
      }]
    }
  }
}

This maps URLs like venelxapp://details/142 and https://app.venelx.com/details/142 directly to your dynamic detail screen. Expo Router parses the id parameter automatically.

Handling Nested Layout Stacks

Combine tab-bar navigation with detail screen stacks using parenthesized group directories. Groups create navigator nesting without adding URL segments:

// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: "#111111",
        tabBarStyle: {
          borderTopWidth: 4,
          borderTopColor: "#111111",
          backgroundColor: "#fdfbf7",
        },
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "Home",
          tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} />,
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: "Search",
          tabBarIcon: ({ color }) => <Ionicons name="search" size={24} color={color} />,
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: "Profile",
          tabBarIcon: ({ color }) => <Ionicons name="person" size={24} color={color} />,
        }}
      />
    </Tabs>
  );
}

Type-Safe Route Parameters

Expo Router provides typed useLocalSearchParams hooks that give you autocomplete and type checking for route parameters:

// app/details/[id].tsx
import { useLocalSearchParams } from "expo-router";
import { View, Text } from "react-native";

export default function DetailScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  
  return (
    <View style={{ padding: 24 }}>
      <Text style={{ fontSize: 24, fontWeight: "900" }}>
        Detail #{id}
      </Text>
    </View>
  );
}

Error Boundaries and Not-Found Handling

Production apps need graceful error handling. Expo Router supports ErrorBoundary exports and the +not-found.tsx convention:

// app/+not-found.tsx
import { Link, Stack } from "expo-router";
import { View, Text } from "react-native";

export default function NotFoundScreen() {
  return (
    <>
      <Stack.Screen options={{ title: "Page Not Found" }} />
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <Text style={{ fontSize: 32, fontWeight: "900" }}>404</Text>
        <Link href="/" style={{ marginTop: 16, color: "#4c8cfc" }}>
          Go Home
        </Link>
      </View>
    </>
  );
}

For route-level error boundaries, export an ErrorBoundary component from any route file:

// app/details/[id].tsx
export function ErrorBoundary({ error }: { error: Error }) {
  return (
    <View style={{ flex: 1, justifyContent: "center", padding: 24 }}>
      <Text style={{ fontSize: 18, fontWeight: "700", color: "red" }}>
        Something went wrong: {error.message}
      </Text>
    </View>
  );
}

For details on automating compilation and publishing in CI, read Expo Prebuild benefits and Shrinking React Native bundles.

References & Citations

← BACK TO ARTICLES