In Expo Router, why does a deep link straight to a modal route leave no screen behind it, and what does unstable_settings.anchor change?
answer
- state built from the URL alone
- stack of one: the modal
- export unstable_settings from layout
- anchor: 'index' renders underneath
- same anchor catches guarded deep links
basics
~20 sA deep link builds the stack from the URL alone, so it holds only the modal. Exporting unstable_settings = { anchor: 'index' } from that stack's layout keeps index rendered underneath, and the same anchor is where guarded deep links land.
solid answer
~40 sWhen a link or cold start opens a modal route such as `/redeem` directly, Expo Router builds the navigation state from the URL, and without more information the stack contains **only the modal**. Dismissing it reveals nothing; on Android, back leaves the app. The fix is an **anchor**: export `unstable_settings = { anchor: 'index' }` from the stack's `_layout.tsx`, and the deep link produces `[index, redeem]`, with home rendered underneath. Each nested stack needs its own anchor, it must name a child route of that layout, and it takes precedence over `initialRouteName`. The same anchor is where `Stack.Protected` sends a deep link to a guarded route.
code
tsx · 15 lines// src/app/_layout.tsx
import { Stack } from 'expo-router';
export const unstable_settings = {
anchor: 'index',
};
export default function Layout() {
return (
<Stack>
<Stack.Screen name="index" />
<Stack.Screen name="redeem" options={{ presentation: 'modal' }} />
</Stack>
);
}go deeper
Recall that a link opening a modal directly can leave nothing underneath, and that an anchor exported from the layout fixes it.
Explain why state built from a URL alone holds only the modal, how anchor adds a base route, and why nested stacks each need one.
Diagnose it from bug reports such as Android back quitting the app, test deep links on cold start, and make close buttons fall back to a known route.
Treat every linkable modal as an entry point: decide its base screen deliberately, since the same anchor also decides where blocked signed-out users land.
## The problem: a modal with nothing behind it In **Expo Router**, a modal route is a screen in a stack whose layout declares it with `presentation: 'modal'` (or `formSheet`). When the user opens it from inside the app, the screen they came from is already on the stack, so dismissing the modal returns to it. A **deep link** is different. When a link or a cold start opens `/redeem` directly, Expo Router builds the navigation state from the URL alone. Without extra information, the stack it builds contains **only the modal**. The consequences: - The modal is the root screen, so there is nothing to reveal when it is dismissed. - The iOS swipe-down or Android back gesture has no previous screen to return to; on Android, back leaves the app. For a streaming app, that is the "Redeem a gift code" modal opened from a link in an email: the user redeems the code, closes the sheet, and finds no catalogue underneath. ## The fix: `unstable_settings.anchor` An **anchor** is the route a stack keeps at its base. You declare it by exporting a settings object from the stack's layout file: ```tsx export const unstable_settings = { anchor: 'index', }; ``` With `anchor: 'index'`, a deep link to `/redeem` produces a stack of `[index, redeem]`: the home screen is rendered **underneath** the modal, and dismissing the modal reveals it. Key facts about the setting: 1. **It is per layout.** In an app with nested stacks, each nested stack that can be deep-linked into needs its own anchor, and its value becomes that stack's initial route. 2. **It must name a direct child route of that layout.** An anchor naming a route the layout does not contain raises an "invalid anchor" error listing the valid options. 3. **It takes precedence over `initialRouteName`.** Both live on `unstable_settings`; when both are present, Expo Router uses `anchor`. 4. **The `unstable_` prefix is meaningful.** The settings export does not work with async routes, a development-only bundling mode, which is why the API is marked unstable. ## The same anchor serves protected routes The anchor is not only for modals. When a layout uses `Stack.Protected` and a user arrives at a guarded route through a link or a cold start, Expo Router shows the stack's anchor route in its place. For the streaming app: | Setting | Deep link to `/redeem` (modal) | Signed-out link to a guarded `/account` | |---|---|---| | No anchor | Stack holds only the modal | First available screen, often `index` | | `anchor: 'index'` | Home rendered under the modal | Home shown | | `anchor: 'browse'` | Browse rendered under the modal | Browse shown | So one line decides both "what sits behind a deep-linked modal" and "where a blocked user lands". If the anchor itself is guarded out, the stack falls back to its first available screen. ## Diagnosing it in a real app A deep-linked modal without an anchor usually shows up as one of these reports: - "After closing the gift-code screen the app quits" (Android back with a one-screen stack). - "The sheet opened from the email has no screen behind it" (iOS). - "The close button does nothing" (the screen called a back action with no history). The checklist: 1. Open the link on a cold start, not only while the app is running, since only the cold path starts from an empty state. 2. Find the layout that declares the modal and check for an `unstable_settings` export with `anchor`. 3. In nested layouts, check every stack between the root and the modal. 4. Make the close button robust too: check whether there is a screen to go back to, and navigate to a known route when there is not. ## What it does not change - Opened from a screen already in the same stack, the modal still sits directly above that screen; the anchor matters when the stack is built from a URL. - It does not make the modal's presentation different; `presentation` stays on the `Stack.Screen` options. - It does not register the link with the operating system; that is URL-scheme and universal-link setup. This is the behaviour of Expo SDK 57 (Expo Router 57.x); the Expo documentation describes anchoring as essential whenever a modal route can be reached by a deep link.
- The modal lives in a nested stack under src/app/(browse)/. Where does the anchor go?In the layout of the stack that declares the modal, here `src/app/(browse)/_layout.tsx`, naming one of that layout's own child routes. Anchors are per layout: each nested stack a deep link can enter needs one, and its value becomes that stack's initial route. An anchor on the root layout alone does not put a screen underneath a modal inside a nested stack.
- How does the anchor interact with Stack.Protected?When a cold start or deep link targets a route whose guard is false, Expo Router shows the stack's anchor route instead. So `anchor: 'browse'` sends a signed-out viewer who opens an account link to the browse screen. If the anchor route is itself guarded out, the stack falls back to its first available screen.
saying these in an interview costs you the question
- A deep link always restores the screen the user was last on
- Setting the anchor changes how a modal is presented
- One anchor on the root layout covers every nested stack
- initialRouteName wins over anchor when both are set
- An anchor can name any route in the app, not just a child