Skip to main content

File-Based Routing

DPM Delivery Mobile uses Expo Router for navigation, which provides a file-based routing system. Routes are automatically generated based on the file structure in the src/app/ directory.

Route Structure

src/app/
├── _layout.tsx                   # Root layout
├── (public)/                     # Public route group
│   ├── _layout.tsx
│   └── (auth)/
│       └── sign-in.tsx          # Route: /(public)/(auth)/sign-in
└── (parcel)/                     # Protected route group
    ├── _layout.tsx              # Auth guard
    ├── (tabs)/                   # Tab navigator group
    │   ├── _layout.tsx
    │   ├── index.tsx            # Route: /(parcel)/(tabs)/
    │   ├── history.tsx          # Route: /(parcel)/(tabs)/history
    │   ├── transactions.tsx     # Route: /(parcel)/(tabs)/transactions
    │   └── profile.tsx          # Route: /(parcel)/(tabs)/profile
    └── (stack)/                  # Stack navigator group
        ├── _layout.tsx
        ├── request-payment.tsx   # Route: /(parcel)/(stack)/request-payment
        └── shipments/[reference]/index.tsx  # Dynamic route

Root Layout

The root layout sets up global providers and defines top-level navigation:
// src/app/_layout.tsx
import { Providers } from "@/components/providers";
import { useColorScheme } from "@/hooks/use-color-scheme";
import { ThemeProvider } from "@react-navigation/native";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import "../global.css";

export const unstable_settings = {
  anchor: "(public)/auth/sign-in",
  initialRouteName: "(public)/auth/sign-in",
};

export default function RootLayout() {
  const colorScheme = useColorScheme();

  return (
    <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
      <Providers>
        <Stack>
          <Stack.Screen name="(public)" options={{ headerShown: false }} />
          <Stack.Screen name="(parcel)" options={{ headerShown: false }} />
        </Stack>
        <StatusBar style="auto" />
      </Providers>
    </ThemeProvider>
  );
}

Key Features:

  • unstable_settings: Defines initial route and anchor
  • Providers: Wraps entire app with necessary providers (TanStack Query, Gesture Handler, etc.)
  • Stack: Root stack navigator with two main route groups

Route Groups

Route groups use parentheses () to organize routes without affecting the URL structure.

(public) Route Group

Handles unauthenticated routes like sign-in:
// src/app/(public)/_layout.tsx
import { Stack } from "expo-router";

export default function PublicLayout() {
  return <Stack screenOptions={{ headerShown: false }} />;
}
Routes:
  • /(public)/(auth)/sign-in - Sign in page

(parcel) Route Group

Handles authenticated routes with auth guard:
// src/app/(parcel)/_layout.tsx
import { User } from "@/types/auth.types";
import { Storage, StorageKeys } from "@/utils/storage";
import { Redirect, Stack } from "expo-router";

export default function PublicLayout() {
  const user = Storage.getObject(StorageKeys.USER) as User;
  if (!user) {
    return <Redirect href="/(public)/(auth)/sign-in" />;
  }

  return <Stack screenOptions={{ headerShown: false }} />;
}
Auth Guard Logic:
  1. Check if user exists in storage
  2. Redirect to sign-in if not authenticated
  3. Render child routes if authenticated

Tab Navigation

The (tabs) group creates a bottom tab navigator:
// src/app/(parcel)/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { HapticTab } from "@/components/haptic-tab";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";

export default function TabLayout() {
  const colorScheme = useColorScheme();

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
        headerShown: false,
        tabBarButton: HapticTab,
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: "Home",
          tabBarIcon: ({ color }) => (
            <IconSymbol size={28} name="house.fill" color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="history"
        options={{
          title: "History",
          tabBarIcon: ({ color }) => (
            <IconSymbol size={28} name="clock.arrow.circlepath" color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="transactions"
        options={{
          title: "Transactions",
          tabBarIcon: ({ color }) => (
            <IconSymbol size={28} name="arrow.left.arrow.right" color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: "Profile",
          tabBarIcon: ({ color }) => (
            <IconSymbol size={28} name="person.fill" color={color} />
          ),
        }}
      />
    </Tabs>
  );
}
Features:
  • Haptic feedback on tab press
  • SF Symbols icons (iOS) with fallback
  • Theme-aware colors
  • Four main tabs: Home, History, Transactions, Profile

Stack Navigation

The (stack) group creates a stack navigator for modal-like screens:
// src/app/(parcel)/(stack)/_layout.tsx
import { Ionicons } from "@expo/vector-icons";
import { Stack, useRouter } from "expo-router";
import { Pressable } from "react-native";

export default function StackLayout() {
  const router = useRouter();

  const goBack = () => {
    if (router.canGoBack()) {
      router.back();
      return;
    }
    router.replace("/(parcel)/(tabs)");
  };

  return (
    <Stack>
      <Stack.Screen
        name="shipments/[reference]/index"
        options={{
          headerShown: true,
          headerShadowVisible: false,
          title: "Order details",
          headerLeft: () => (
            <Pressable
              onPress={goBack}
              className="w-9 h-9 rounded-full bg-gray-100 items-center justify-center"
            >
              <Ionicons name="arrow-back" size={18} color="black" />
            </Pressable>
          ),
        }}
      />
      <Stack.Screen
        name="request-payment"
        options={{
          headerShown: true,
          headerShadowVisible: false,
          title: "",
          headerLeft: () => (
            <Pressable
              onPress={goBack}
              className="w-9 h-9 rounded-full bg-gray-100 items-center justify-center"
            >
              <Ionicons name="arrow-back" size={18} color="black" />
            </Pressable>
          ),
        }}
      />
    </Stack>
  );
}
Features:
  • Custom back button with fallback to tabs
  • Header configuration per screen
  • Safe navigation handling

Dynamic Routes

Dynamic segments use square brackets []:
// src/app/(parcel)/(stack)/shipments/[reference]/index.tsx
import { useLocalSearchParams } from "expo-router";

export default function ShipmentDetailsPage() {
  const { reference } = useLocalSearchParams<{ reference: string }>();

  // Use reference to fetch shipment details
  const { data: shipment } = useQuery(
    getShipmentByReferenceQueryOptions(reference)
  );

  return (
    // ... render shipment details
  );
}
Route Pattern: /shipments/:reference Example URLs:
  • /shipments/SHP-12345
  • /shipments/ORD-67890

Programmatic Navigation

import { useRouter } from "expo-router";

const router = useRouter();

// Navigate to route
router.push("/(parcel)/(tabs)/profile");

// Navigate with parameters
router.push({
  pathname: "/shipments/[reference]",
  params: { reference: "SHP-12345" },
});

// Go back
router.back();

// Replace current route
router.replace("/(public)/(auth)/sign-in");
import { Link } from "expo-router";

<Link href="/(parcel)/(tabs)/profile">
  <Text>Go to Profile</Text>
</Link>

// With parameters
<Link
  href={{
    pathname: "/shipments/[reference]",
    params: { reference: shipment.reference },
  }}
>
  <Text>View Details</Text>
</Link>

Conditional Redirects

import { Redirect } from "expo-router";

if (!user) {
  return <Redirect href="/(public)/(auth)/sign-in" />;
}

Type-Safe Routes

Expo Router provides TypeScript autocomplete for routes. Configure in tsconfig.json:
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
  1. Use route groups for organizing related screens without affecting URLs
  2. Implement auth guards at layout level for protected routes
  3. Handle back navigation safely with canGoBack() checks
  4. Use typed parameters with useLocalSearchParams<T>()
  5. Prefer replace() for auth flows to prevent back navigation
  6. Use descriptive names for dynamic segments: [reference], [id], etc.

Build docs developers (and LLMs) love