EthicsComputerCourses
Join training +91 90532 10052

Navigation with Expo Router

React Native · Lesson 11 of 30 Beginner 7 min read Updated

By the end of this lesson you will be able to build an app with several screens and move between them. You will create a stack of screens, link them, pass an ID to a details screen, and control the back button.

Expo Router is the navigation system in new Expo projects. It is built on top of React Navigation, the long-standing navigation library for React Native, and adds file-based routing: your files decide your screens.

How mobile navigation feels

On the web, every page has a URL and the browser keeps a history. On a phone, screens slide in from the side and pile up, like plates stacked in a canteen. Opening a screen puts a new plate on top (a push). Pressing back takes the top plate off (a pop). This pile is called a stack.

Practise the idea first. This browser demo keeps a stack in state with a reducer. It is plain React, so it runs here.

React JSXTry it yourself
import { useReducer } from "react";
import { createRoot } from "react-dom/client";

function stackReducer(stack, action) {
  switch (action.type) {
    case "push":
      return [...stack, action.screen];
    case "back":
      return stack.length > 1 ? stack.slice(0, -1) : stack;
    case "replace":
      return [...stack.slice(0, -1), action.screen];
    default:
      return stack;
  }
}

function FakePhone() {
  const [stack, dispatch] = useReducer(stackReducer, ["Home"]);
  const current = stack[stack.length - 1];

  return (
    <div style={{ fontFamily: "sans-serif" }}>
      <p>Stack: {stack.join(" > ")}</p>
      <h3>Now showing: {current}</h3>
      <button onClick={() => dispatch({ type: "push", screen: "Shop 7" })}>Open Shop 7</button>{" "}
      <button onClick={() => dispatch({ type: "push", screen: "Cart" })}>Open Cart</button>{" "}
      <button onClick={() => dispatch({ type: "replace", screen: "Payment done" })}>Replace with Payment done</button>{" "}
      <button onClick={() => dispatch({ type: "back" })}>Back</button>
    </div>
  );
}

createRoot(document.getElementById("root")).render(<FakePhone />);

Press "Open Shop 7", then "Open Cart". The stack reads "Home > Shop 7 > Cart". Press "Replace with Payment done" and Cart is swapped out, so Back now goes straight to Shop 7. Expo Router does this bookkeeping for you, but the idea is exactly the same.

Step 1: a root layout with a Stack

Start from a blank project (npm run reset-project). Open src/app/_layout.jsx (or .tsx) and render a Stack. Every screen in src/app now becomes a page in this stack.

React JSX
// src/app/_layout.jsx
import { Stack } from "expo-router";

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerStyle: { backgroundColor: "#087ea4" }, headerTintColor: "white" }}>
      <Stack.Screen name="index" options={{ title: "Rohtak Bazaar" }} />
      <Stack.Screen name="about" options={{ title: "About us" }} />
    </Stack>
  );
}

What you should see: a blue header bar with the white title "Rohtak Bazaar" above your home screen. screenOptions applies to every screen, and each Stack.Screen sets options for one file. The name is the file name without the extension.

Create src/app/about.jsx and link to it from the home screen with Link.

React JSX
// src/app/index.jsx
import { Link } from "expo-router";
import { View, Text, Pressable } from "react-native";

export default function Home() {
  return (
    <View style={{ flex: 1, padding: 24, gap: 16 }}>
      <Text style={{ fontSize: 18 }}>Fresh fruit and sweets since 1982.</Text>
      <Link href="/about">Read our story</Link>
      <Link href="/about" asChild>
        <Pressable style={{ backgroundColor: "#087ea4", padding: 12, borderRadius: 8 }}>
          <Text style={{ color: "white", textAlign: "center" }}>About us (button)</Text>
        </Pressable>
      </Link>
    </View>
  );
}

What you should see: a text link and a blue button. Tapping either slides the About screen in from the right, with a back arrow in its header. Tapping back, or swiping from the left edge on iPhone, returns home.

By default Link renders a Text. With asChild, it passes its press handling to your own child, such as a styled Pressable.

Step 3: dynamic routes and params

A shop app has many shops but one shop screen. Name the file with square brackets: src/app/shops/[id].jsx. The part in brackets becomes a param, a value read from the URL.

React JSX
// src/app/shops/[id].jsx
import { Stack, useLocalSearchParams } from "expo-router";
import { View, Text } from "react-native";

const shops = { "7": { name: "Gupta Sweets", city: "Rohtak" }, "9": { name: "Kerala Spices", city: "Kochi" } };

export default function ShopScreen() {
  const { id, from } = useLocalSearchParams();
  const shop = shops[id];

  if (!shop) {
    return <Text style={{ padding: 24 }}>Shop {id} was not found.</Text>;
  }

  return (
    <View style={{ padding: 24 }}>
      <Stack.Screen options={{ title: shop.name }} />
      <Text style={{ fontSize: 20 }}>{shop.name}, {shop.city}</Text>
      {from ? <Text>You came from: {from}</Text> : null}
    </View>
  );
}

Link to it with a string or an object:

React JSX
<Link href="/shops/7">Gupta Sweets</Link>
<Link href={{ pathname: "/shops/[id]", params: { id: "9", from: "offers" } }}>Kerala Spices</Link>

What you should see: tapping "Gupta Sweets" opens a screen whose header title is "Gupta Sweets" and whose body says "Gupta Sweets, Rohtak". Tapping "Kerala Spices" also shows "You came from: offers", because extra params that do not match a bracket become query parameters (/shops/9?from=offers).

Rendering <Stack.Screen options={...} /> inside a screen sets that screen's options from its own data, which is how the header shows the shop's name.

Step 4: navigate from code with router

Sometimes you navigate after something happens, such as a successful payment. Import router (or call useRouter()) and use its methods:

React JSX
import { router } from "expo-router";

async function placeOrder(cart) {
  const orderId = await saveOrder(cart); // your own function
  router.replace({ pathname: "/orders/[id]", params: { id: String(orderId) } });
}

// other methods:
// router.push("/cart")      add a screen on top of the stack
// router.navigate("/cart")  go to /cart, reusing it if it is already in the stack
// router.back()             go back one screen
// router.dismissAll()       close every screen above the first one in this stack

Use push for normal forward movement. Use replace when the user should not come back with the back button, such as after login or payment. Nobody wants to press back and land on the payment form again.

Typed routes and the not-found screen

New projects turn on typed routes in app.json. In .tsx files your editor then warns you about links to screens that do not exist, such as /abuot. You can also add src/app/+not-found.jsx to show a friendly screen for unknown links.

Common mistakes

  • Placing a screen outside src/app. Only files inside src/app are routes.
  • Forgetting that params are strings. id === 7 is always false; use Number(id) === 7.
  • Using push after login. The back button then returns to the login form. Use replace.
  • A Stack.Screen name that does not match a file. The options are ignored. Check the spelling of name.
  • Wrapping a Pressable in Link without asChild. You get a text element around your button; add asChild.

Key points

  • Expo Router is file-based: files in src/app are screens, and paths are URLs.
  • A Stack in _layout shows screens as a pile with a header and back button.
  • Link with href navigates; asChild lets your own component be the link.
  • [id].jsx makes a dynamic route; read values with useLocalSearchParams().
  • router.push, replace, navigate and back navigate from code.
  • Render <Stack.Screen options={...} /> in a screen to set its own title.

Exercise

Build a three-screen app for a college canteen. The home screen lists three dishes with Links. Each link opens src/app/dishes/[id].jsx, which shows the dish name as the header title and its price in the body. Add an "Order" button on the dish screen that uses router.replace to go to src/app/thanks.jsx, which says "Order placed for DISH". Pass the dish name as a param.

Show solution
React JSX
// src/app/dishes/[id].jsx
import { Stack, router, useLocalSearchParams } from "expo-router";
import { View, Text, Pressable } from "react-native";

const dishes = { "1": { name: "Chole bhature", price: 70 }, "2": { name: "Masala dosa", price: 60 }, "3": { name: "Veg thali", price: 120 } };

export default function Dish() {
  const { id } = useLocalSearchParams();
  const dish = dishes[id];
  if (!dish) return <Text>Dish not found</Text>;

  return (
    <View style={{ padding: 24, gap: 16 }}>
      <Stack.Screen options={{ title: dish.name }} />
      <Text style={{ fontSize: 20 }}>₹{dish.price}</Text>
      <Pressable onPress={() => router.replace({ pathname: "/thanks", params: { dish: dish.name } })}>
        <Text>Order</Text>
      </Pressable>
    </View>
  );
}

// src/app/thanks.jsx
import { useLocalSearchParams } from "expo-router";
import { Text } from "react-native";

export default function Thanks() {
  const { dish } = useLocalSearchParams();
  return <Text style={{ padding: 24 }}>Order placed for {dish}</Text>;
}

On the home screen, links such as <Link href="/dishes/1">Chole bhature</Link> open the dish screen. Because the Order button uses replace, pressing back on the thanks screen goes home, not to the dish screen.

Quick Quiz

1. Which file creates a screen at the URL /shops/42?

2. How do you read the id param inside a screen?

3. After a successful login, which method should send the user to the home screen?

4. What type is a param such as id when it arrives?

Learn React Native with a mentor

Free lessons get you started. Our Mobile App Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call