skip to content

With Expo Router, a cold-start deep link to /cookbook/desserts opens with no back button; how does exporting unstable_settings.initialRouteName from a _layout fix that?

level: seniorimportance: nice to knowfreq 22%

answer

  1. the stack holds one screen
  2. history is built from the URL
  3. exported from the stack's _layout
  4. must name a real child route
  5. in-app pushes need withAnchor

basics

~20 s

A deep link builds navigation state from the URL alone, so the cookbook stack holds only desserts; exporting unstable_settings = { initialRouteName: 'index' } from cookbook/_layout.tsx makes Expo Router place index beneath it, restoring a back target.

solid answer

~40 s

When the app opens straight onto `/cookbook/desserts`, Expo Router builds the navigation state from the URL, so the cookbook stack contains only `desserts` and there is nothing to go back to. Exporting `unstable_settings = { initialRouteName: 'index' }` from `src/app/cookbook/_layout.tsx` tells Expo Router to insert `index` beneath the deep-linked screen; the same export in the root layout with `'(tabs)'` puts the tab bar under a root-stack screen. The value must be one of that layout's child route names, or route setup throws with the list of valid names. It applies when the app opens or reloads onto a deeper route; ordinary in-app navigation into a stack does not insert it unless the link or router call asks with `withAnchor`. The API is marked unstable and does not work with async routes in development.

code

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

export const unstable_settings = {
  // A deep link to /cookbook/desserts gets index beneath it.
  initialRouteName: 'index',
};

export default function CookbookLayout() {
  return <Stack />;
}

go deeper

for a junior

Recall that a deep link can open a stack with only one screen and that unstable_settings.initialRouteName in the layout file puts a screen underneath it.

for a middle

Explain that navigation state on a deep link is derived from the URL alone, and that the value must be a child route name relative to the layout.

for a senior

Diagnose the cold-start-only missing back button, fix it in the right layout at each level, and note the withAnchor gap for in-app links and the async-routes caveat.

for a principal

Decide which layouts in a large app need an initial route, so every link a notification or campaign can send lands with a sensible back path.

## The symptom In the recipe app, `src/app/cookbook/_layout.tsx` returns a `Stack` with `index.tsx` (the list of collections) and `desserts.tsx`. Tapping through the app works: index, then desserts, and the header shows a back button. But when a notification or a shared link **cold-starts** the app on `/cookbook/desserts`, the screen appears with **no back button**, and on web a reload of that URL shows the same thing. ## Why the stack holds one screen A deep link carries only a URL. Expo Router turns the URL into **navigation state** by walking the route tree and picking the matching route in each layout. Nothing in `/cookbook/desserts` says that `index` was ever visited, so the resulting state is: 1. the root stack, with the `cookbook` layout as its only route; 2. the cookbook stack, with `desserts` as its only route. A stack with one screen has no back target, so the header omits the back button and the Android back action falls through to the parent navigator, or closes the app when there is none. ## unstable_settings.initialRouteName A layout file can export a static object called **`unstable_settings`**. Its **`initialRouteName`** names the child route that should sit at the bottom of that layout's navigator when the state is built from a URL: - `export const unstable_settings = { initialRouteName: 'index' }` in `cookbook/_layout.tsx` produces a cookbook stack of `index` then `desserts`, so back returns to the collection list. - The same export with `initialRouteName: '(tabs)'` in the **root** layout puts the tab navigator under a root-stack screen, so a deep link to `/settings` can go back to the Recipes tab. With both exports, a cold start on `/cookbook/desserts` produces a back path of desserts, cookbook index, then the tabs. ## Rules and caveats | Rule | What happens | |---|---| | Value must be a child route name of that layout | Otherwise route setup throws that the layout has an invalid anchor and lists the valid names | | Names follow layout-relative naming | Groups keep parentheses (`'(tabs)'`), folders without a layout keep their path | | Applies when state comes from a URL | Cold-start deep links and web reloads get the inserted screen | | In-app navigation into the stack | Does not insert it by default; `Link` and router calls accept `withAnchor` to opt in | | Async routes in development | `unstable_settings` does not work with them, which is why it is named unstable | Two further details come from the source. The same object also accepts an **`anchor`** key, which takes precedence over `initialRouteName` and is the name used by the modal and protected-route guides. And when a layout is shared by several groups through array syntax, a nested object keyed by the group name can set a different initial route per group. ## Walking through the fixed state With `initialRouteName: '(tabs)'` exported from the root layout and `initialRouteName: 'index'` exported from the cookbook layout, a cold start on `/cookbook/desserts` builds this state: 1. The root stack holds `(tabs)` at the bottom and `cookbook` on top. 2. The cookbook stack holds `index` at the bottom and `desserts` on top. 3. The first back action pops `desserts` and shows the collection list. 4. The second back action pops the whole `cookbook` route off the root stack and shows the tab bar. The user now experiences the link exactly as if they had tapped through the app, which is the whole purpose of the setting. Each layout is responsible only for its own level; nothing in the deep-linked screen knows about any of this. ## Choosing the value - Pick the screen a user would naturally have come from: the folder's `index`, or the tab group for root-level screens. - Prefer a screen that renders without extra context, such as the folder's `index`, since nothing in the URL describes it. - Keep it in sync with file renames, because a stale name now throws rather than warning. ## Why it is a senior question The bug appears only on **cold start from a link**, the path that notifications, marketing links and web reloads take and that manual testing through the UI never exercises. Recognising that navigation state is derived from the URL, and that the fix lives in the layout rather than in the screen, is the production judgment being tested.

  • Why does the fix not apply when the user taps a Link to /cookbook/desserts from the Saved tab?
    In-app navigation into a stack that is not yet mounted makes the target the only screen by default. The inserted initial route applies when state is built from a URL, as on cold start or web reload. A `Link` or router call can opt in with `withAnchor`.
  • What happens if initialRouteName is set to 'desert' by mistake?
    Expo Router validates the value against the layout's child routes while building the tree and throws an invalid-anchor error that lists the valid names, so the typo fails at startup instead of silently producing a one-screen stack.

saying these in an interview costs you the question

  • The deep-linked screen should call navigation.goBack with a fallback instead.
  • initialRouteName is set in app.json under the expo-router plugin.
  • A misspelled initialRouteName is silently ignored.
  • initialRouteName inserts its screen before every in-app push too.
  • A cold-start deep link replays the screens the user visited last session.