EduFlow

Section 3 · Navigation and State

File-Based Routing with Expo Router

18 min3 min readFill in the blanks

Lesson at a glance

Turn files in src/app into Trailhead’s screens with Expo Router, wrap them in a native stack with a layout file, and move between them with Link and the router object.

  1. 1Files are routes
  2. 2Layouts wrap their siblings
  3. 3Moving between screens
  4. 4A stack is not browser history
  5. 5Not-found and why URLs still matter

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

On the web, a URL bar shows where you are, and the back button walks through history. On a phone, users see neither: they see screens sliding in from the right, a back arrow or swipe, and tabs at the bottom. Underneath, Expo Router still gives every screen a URL, which is what makes deep links, typed navigation and a web version possible. You get both models at once.

Files are routes

Expo Router builds your navigation from the files in src/app. The path of the file is the route:

Text
src/app/
  _layout.tsx          wraps everything below in a navigator
  index.tsx            /
  about.tsx            /about
  settings/
    _layout.tsx        wraps the settings screens
    index.tsx          /settings
    units.tsx          /settings/units
  +not-found.tsx       any URL that doesn't match

Each route file default-exports a component. That’s all a screen is:

src/app/about.tsx
import { Text, View } from 'react-native';

export default function About() {
  return (
    <View style={{ flex: 1, padding: 16 }}>
      <Text>Trailhead keeps a private log of your hikes.</Text>
    </View>
  );
}

Because every file in the folder becomes a route, keep everything else elsewhere: src/components, src/lib, src/state. A row component accidentally saved in src/app becomes a screen that anyone can deep-link to.

Layouts wrap their siblings

A _layout.tsx file renders a navigator around the routes in its folder. Trailhead’s root layout uses a Stack, the native push-and-pop navigator with platform animations, swipe-back on iOS and the system back gesture on Android:

src/app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerTintColor: '#2f6f4e' }}>
      <Stack.Screen name="index" options={{ title: 'Trailhead' }} />
      <Stack.Screen name="about" options={{ title: 'About' }} />
    </Stack>
  );
}

screenOptions applies to every screen in the stack; each Stack.Screen customizes one, matched by its route name. You don’t have to list every route: unlisted ones still work with default options.

A few header options cover most needs: title, headerShown: false for screens that draw their own top bar (like the full-screen map from the last lesson), headerBackTitle for the label next to the iOS back arrow, and headerRight for a button such as “Edit”. On iOS, headerLargeTitle: true gives you the big collapsing title from Apple’s own apps, which suits a top-level list like the hike log.

When a title depends on data (the hike’s name, say), render <Stack.Screen options={{ title: hike.name }} /> inside the screen itself. It renders nothing visible; it configures the header of the screen it’s in.

The files index.tsx, settings/index.tsx and settings/units.tsx map to the URLs /, /settings and /settings/units; navigating pushes each onto a stack, and back pops the top one. src/app/ index.tsx settings/index.tsx settings/units.tsx / /settings /settings/units / /settings /settings/units Stack (top is visible) push adds · back pops
Files become routes, and the layout’s Stack holds the visited screens

Moving between screens

For anything the user taps, use Link. It renders text by default; with asChild it hands navigation to your own pressable component:

src/app/index.tsx
import { Link } from 'expo-router';
import { Pressable, Text, View } from 'react-native';

export default function HikeLog() {
  return (
    <View style={{ flex: 1, padding: 16, gap: 12 }}>
      <Link href="/about">About Trailhead</Link>
      <Link href="/settings" asChild>
        <Pressable accessibilityRole="button">
          <Text>Settings</Text>
        </Pressable>
      </Link>
    </View>
  );
}

From code, after a save or a login, use the router object:

TSX
import { router } from 'expo-router';

router.push('/settings/units'); // add a screen on top
router.back();                  // pop the top screen
router.replace('/');            // swap the current screen; no way back to it

push is the normal forward move. replace is for flows where returning makes no sense: after onboarding, after logging in, after a “saved!” confirmation. back is what the header’s back arrow and the swipe gesture already do; call it yourself after a form saves so the user lands where they came from. There’s also router.navigate, which goes to a route and, if that route is already in the stack, returns to it instead of pushing a duplicate.

A stack is not browser history

It’s tempting to treat the stack like browser history, but there’s one difference that matters: every screen in the stack stays mounted. When you push the hike detail screen, the log underneath keeps its state and scroll position, and its effects keep running. That’s why going back is instant, and also why a screen’s useEffect doesn’t re-run when you return to it. When you need to refresh data every time a screen comes back into view, Expo Router provides useFocusEffect, which runs when the screen gains focus and cleans up when it loses it.

Not-found and why URLs still matter

+not-found.tsx renders for any URL that doesn’t match a route, which on mobile happens mostly through deep links from old emails or a renamed screen. Give it a friendly message and a Link home.

Thinking in URLs pays off later in this section: a notification can open /hikes/42 directly, a link in a message can open the app on the right screen, and the same routes run on the web if you ever build Trailhead for it.

Next you’ll add tabs at the bottom of the app, a detail screen for each hike, and pass the hike’s id through the URL.

Key takeaways

  • Every file in src/app is a route, and its path is the URL; keep components, hooks and utilities outside that folder.
  • A _layout.tsx file wraps its sibling routes in a navigator, such as a native Stack.
  • Use <Link href> for navigation the user taps, and router.push, router.replace and router.back from code.
  • push adds a screen to the stack, back pops it, and replace swaps the current screen so the user can’t go back to it.

Further reading

Check your understanding

3 questions

Question 1 of 3

You add src/app/HikeRow.tsx to hold a reusable list row component. What happens?

Question 2 of 3

After a user finishes onboarding, Trailhead navigates to the hike log. Pressing back should not return to onboarding. Which call fits?

Question 3 of 3

Where does a header title for the /settings screen belong?

Practice

Fill in the blanks

Map Trailhead's URLs to files

Fill in the file names (without .tsx) so Expo Router renders each URL from the right file, wraps the settings screens in their own layout, and shows a friendly screen for unknown URLs. Then pick the router method that returns from /settings/units to /settings after a save without stacking a duplicate screen.

# URL file that renders it / src/app/.tsx /settings src/app/settings/.tsx /settings/units src/app/settings/.tsx (Stack around settings/*) src/app/settings/.tsx (any unknown URL) src/app/.tsx # In code, after the units form saves, return to /settings: router.();