Expo Router

File-based routing: a file in app/ is a route, _layout.tsx is the navigator, and Link is not an anchor tag.

Syntax<Link href="/settings">Settings</Link> // same as useRouter().navigate('/settings')

Expo Router gives the app a file system for routes. A file in the app directory is a route: app/settings.tsx becomes /settings. There is no route table to register and nothing to keep in sync.

The conventions

  • index.tsx — the default route of its folder.
  • _layout.tsx — not a screen. It renders the navigator that wraps every route in its folder. app/_layout.tsx is the root layout and the app's entry point.
  • [id].tsx — a dynamic segment, matching one path segment.
  • [...rest].tsx — a catch-all, matching the rest of the path.
  • (group) — a folder whose name is in parentheses. It groups files and contributes nothing to the path.
  • +not-found.tsx — the route for anything that matches no file.

Moving between routes

Link is the declarative form and useRouter is the same thing in code. Link is not an anchor tag: on iOS and Android it renders a Text and accepts Text props, and href names a route inside app rather than a URL a server answers. Pass asChild to hand the press to your own Pressable instead.

Underneath

Expo Router is a layer over React Navigation. The layouts you write produce React Navigation navigators, so the screen options are the ones in the React Navigation documentation. Route typing is still opt-in in SDK 57: set experiments.typedRoutes to true in the app config and the CLI generates an expo-env.d.ts that types Link and the router hooks.

Example

Example · tsx
// app/index.tsx - the route at "/"
// On screen: a "Home" heading with two tappable lines under it; the first opens
// the settings screen, the second opens the detail screen for order 1042.
import { Link } from 'expo-router';
import { StyleSheet, Text, View } from 'react-native';

export default function Index() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Home</Text>

      {/* href is a route path inside app/, not a URL a server answers.
          The string child is fine here: Link renders a Text on native. */}
      <Link href="/settings" style={styles.link}>
        Settings
      </Link>

      {/* A dynamic route, written as the file's path plus its params.
          The plain string "/order/1042" works too and means the same thing. */}
      <Link href={{ pathname: '/order/[id]', params: { id: '1042' } }} style={styles.link}>
        Order 1042
      </Link>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24, gap: 16 },
  title: { fontSize: 24, fontWeight: '600' },
  link: { fontSize: 16, color: '#0a7ea4' },
});

When to use it

  • Adding a new screen to an app without touching any navigation configuration.
  • Turning a list row into a tappable link to that record's detail screen.
  • Deciding where app-wide providers such as a theme or a font loader should live.

More examples

The root layout

This is where app-wide setup belongs: a theme provider, a font load, an error boundary. Every route renders inside it, so anything you wrap here applies everywhere, which is the only kind of inheritance this framework has.

Example · tsx
// app/_layout.tsx - the one route file that wraps every other route
import { Stack } from 'expo-router';

export default function RootLayout() {
  // <Stack /> is the self-closing shorthand for <Stack></Stack>. Identical.
  // Files inside app/ become routes whether or not you list them here;
  // Stack.Screen exists to give a route options.
  return (
    <Stack screenOptions={{ headerTitleAlign: 'center' }}>
      <Stack.Screen name="index" options={{ title: 'Home' }} />
      <Stack.Screen name="settings" options={{ title: 'Settings' }} />
      {/* +not-found.tsx catches every path that matches no file */}
      <Stack.Screen name="+not-found" options={{ title: 'Not found' }} />
    </Stack>
  );
}

Link, asChild, and the imperative form

Reach for asChild the moment a link needs to look like a button, a card or a list row. Reach for useRouter when something has to happen before the navigation, such as saving a draft.

Example · tsx
// app/settings.tsx
import { Link, useRouter } from 'expo-router';
import { Pressable, StyleSheet, Text, View } from 'react-native';

export default function Settings() {
  const router = useRouter(); // the imperative form of Link

  return (
    <View style={styles.container}>
      {/* asChild stops Link rendering its own Text and hands the press
          to the child instead, so the child keeps full control of layout. */}
      <Link href="/" asChild>
        <Pressable style={styles.button}>
          <Text style={styles.buttonText}>Home</Text>
        </Pressable>
      </Link>

      {/* The same destination, decided in code rather than in markup.
          Use this form when a condition or an await comes first. */}
      <Pressable style={styles.button} onPress={() => router.push('/')}>
        <Text style={styles.buttonText}>Home, imperatively</Text>
      </Pressable>

      <Pressable style={styles.button} onPress={() => router.back()}>
        <Text style={styles.buttonText}>Back</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24, gap: 12 },
  button: { padding: 14, borderRadius: 8, backgroundColor: '#0a7ea4' },
  buttonText: { color: '#fff', fontSize: 16, textAlign: 'center' },
});

A route inside a group

Shows the two things that confuse people first: a group folder that is invisible in the path, and a Link used as a list row. Each row is tappable and opens the matching post route.

Example · tsx
// app/(tabs)/index.tsx
// The folder name is in parentheses, so this file's path is "/", not "/(tabs)".
// Groups organise files and give them a shared layout; they never appear
// in a path, which means moving a file into one does not break its links.
import { Link } from 'expo-router';
import { FlatList, StyleSheet, Text } from 'react-native';

type Post = { id: string; title: string };

const POSTS: Post[] = [
  { id: 'a1', title: 'Shipping on Friday' },
  { id: 'a2', title: 'Why the build broke' },
];

export default function Feed() {
  return (
    <FlatList
      data={POSTS}
      keyExtractor={(post) => post.id}
      // A list of unknown length belongs in a FlatList, never a ScrollView.
      renderItem={({ item }) => (
        <Link href={`/post/${item.id}`} style={styles.row}>
          {item.title}
        </Link>
      )}
    />
  );
}

const styles = StyleSheet.create({
  row: { padding: 16, fontSize: 16 },
});

Discussion

  • Be the first to comment on this lesson.