ScaleRocket/Mobile

Expo Router

File-based routing deep dive with route groups, layouts, stack vs tabs, typed routes, and linking.

Expo Router

ScaleRocket Mobile uses Expo Router for file-based routing. Every file in the app/ directory automatically becomes a navigable screen.

How It Works

Expo Router maps files to routes:

app/
├── index.tsx          →  /
├── about.tsx          →  /about
├── settings/
│   ├── index.tsx      →  /settings
│   └── profile.tsx    →  /settings/profile

Route Groups

Parentheses () create groups that share a layout without affecting the URL:

app/
├── (auth)/
│   ├── _layout.tsx    # Stack navigator for auth screens
│   ├── login.tsx      →  /login
│   └── register.tsx   →  /register
├── (tabs)/
│   ├── _layout.tsx    # Tab navigator for main app
│   ├── index.tsx      →  /
│   └── profile.tsx    →  /profile

The group name (auth) does not appear in the URL. /login is the route, not /(auth)/login.

Layouts

Every _layout.tsx file defines navigation structure for its directory:

Stack Layout (Auth)

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

export default function AuthLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="login" />
      <Stack.Screen name="register" />
      <Stack.Screen name="forgot-password" />
    </Stack>
  );
}

Tab Layout (Main App)

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

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ headerShown: false }}>
      <Tabs.Screen
        name="index"
        options={{
          title: "Home",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: "Profile",
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Use the router object or Link component:

import { router } from "expo-router";
import { Link } from "expo-router";

// Programmatic navigation
router.push("/profile");
router.replace("/login");    // Replace current screen (no back)
router.back();               // Go back

// Declarative navigation
<Link href="/profile">
  <Text>Go to Profile</Text>
</Link>

Dynamic Routes

Square brackets [] create dynamic segments:

app/
├── user/
│   └── [id].tsx       →  /user/123
// app/user/[id].tsx
import { useLocalSearchParams } from "expo-router";

export default function UserScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();
  return <Text>User ID: {id}</Text>;
}

Typed Routes

Enable typed routes in app.json for autocomplete:

{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

After enabling, TypeScript will autocomplete route paths:

router.push("/profile");   // ✓ Valid route
router.push("/nonexist");  // ✗ Type error

Present screens as modals:

// app/_layout.tsx
<Stack>
  <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
  <Stack.Screen
    name="modal"
    options={{ presentation: "modal", headerTitle: "Modal" }}
  />
</Stack>
// Navigate to modal from anywhere
router.push("/modal");

Deep Linking

Expo Router handles deep links automatically. Configure your scheme in app.json:

{
  "expo": {
    "scheme": "scalerocket"
  }
}

The URL scalerocket:///profile opens the profile screen. See the Deep Linking guide for full setup.

Not Found Screen

Add a catch-all for unmatched routes:

// app/+not-found.tsx
export default function NotFound() {
  return (
    <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
      <Text>Page not found</Text>
      <Link href="/">Go home</Link>
    </View>
  );
}

On this page