In Expo Router, how do router.back, dismiss, dismissTo and dismissAll differ when closing a multi-step refill flow inside a stack?
answer
- back: whatever the focused navigator does
- dismiss: pop count screens
- dismissTo: pop until href, else replace
- dismissAll: first screen of the stack
- canGoBack and canDismiss guard
basics
~20 srouter.back goes back however the focused navigator defines it; dismiss(count) pops that many stack screens; dismissTo(href) pops until href is on top or replaces the current screen if it is absent; dismissAll returns to the nearest stack's first screen.
solid answer
~40 sTake a refill stack: `/refill/select`, `/refill/pharmacy`, `/refill/confirm`. `router.back()` dispatches a generic go-back that the focused navigator handles its own way, one screen in a stack, possibly the previous tab in a tab navigator. `router.dismiss()` is a **stack pop**: `dismiss(2)` from confirm lands on select, and if the current screen is its stack's only route, the whole nested stack is dismissed. `router.dismissTo('/refill/select')` pops until that href is on top; if it is not in the history, it **replaces** the current screen with it instead of stacking. `router.dismissAll()` returns to the **first screen of the closest stack**, like a pop-to-top. `router.canGoBack()` and `router.canDismiss()` report whether back or a stack pop is possible, and `canGoBack()` returns `false` before navigation is ready, so guard calls from code that can run on a cold start.
code
tsx · 21 lines// src/app/refill/confirm.tsx
import { router } from 'expo-router';
import { Button, View } from 'react-native';
export default function ConfirmRefill() {
return (
<View>
<Button title="Change pharmacy" onPress={() => router.dismiss()} />
<Button title="Start over" onPress={() => router.dismissAll()} />
<Button
title="Submit"
onPress={async () => {
await submitRefill();
router.dismissTo('/orders'); // pops to orders if present, else replaces
}}
/>
</View>
);
}
declare function submitRefill(): Promise<void>;go deeper
Recall that back goes back one step, dismissAll returns to the first screen of the stack, and dismissTo returns to a named route.
Explain that dismiss is a stack pop with a count, that dismissTo replaces when the href is absent, and how back differs inside tab navigators.
Build robust flow exits: prefer dismissTo for known targets, guard with canGoBack or canDismiss, and handle cold-start screens where there is no history.
Standardise how multi-step flows close so cancel, restart and finish behave the same everywhere, including when entered from links or notifications.
## The refill flow In the pharmacy app, refilling a prescription is a three-step flow inside its own stack, `src/app/refill/_layout.tsx`: 1. `/refill/select` chooses the prescription; 2. `/refill/pharmacy` chooses the pickup pharmacy; 3. `/refill/confirm` reviews and submits. After submitting, or when the user cancels midway, code has to close part or all of that flow. Expo Router's `router` object offers four closing calls with different reach. ## back: the navigator decides `router.back()` dispatches a generic **go back**. The focused navigator interprets it: - in a **stack**, it pops one screen; - in a **tab navigator**, it may return to the previously selected tab, depending on that navigator's back behaviour; - if the focused navigator cannot go back, the action **bubbles** to its parent. It mirrors the hardware back button and the header back arrow. Use it when "whatever back means here" is what you want. ## dismiss: pop screens from a stack `router.dismiss(count)` dispatches a **stack pop** of `count` screens, defaulting to one: - `dismiss()` from `/refill/confirm` returns to `/refill/pharmacy`; - `dismiss(2)` returns to `/refill/select`; - if the current screen is the **only route** in its stack, dismissing closes that **entire nested stack**, the natural way to close a flow presented as a modal. Because it is a stack action, a tab navigator does not handle it; it goes to the nearest stack. That is the practical difference from `back`. ## dismissTo: pop until a route, else replace `router.dismissTo(href)` pops screens until the **given href is on top**: - from `/refill/confirm`, `dismissTo('/refill/select')` pops two screens; - if the href is **not in the history**, it does not push; it **replaces** the current screen with the target, so no half-finished step is left behind. `<Link dismissTo href="...">` gives the same behaviour declaratively, for example on a "Close" link. ## dismissAll: back to the first screen `router.dismissAll()` returns to the **first screen of the closest stack**, equivalent to a stack pop-to-top. From anywhere inside the refill stack it lands on `/refill/select`. It does not leave the stack; to leave it entirely, dismiss from its first screen or use `dismissTo` with a route outside it. ## Guards: canGoBack and canDismiss | Call | Returns true when | |---|---| | `router.canGoBack()` | the navigation tree can handle a go-back action | | `router.canDismiss()` | some stack in the focused chain holds more than one screen | `canGoBack()` returns **false before navigation is mounted**, for example when called too early from the root layout. A common fallback, used by Expo Router's own not-found screen, is `router.canGoBack() ? router.back() : router.replace('/')`. ## Flows presented as a modal The refill flow is often presented as a **modal** over the main app: the root stack shows a `refill` route with modal presentation, and the refill layout renders its own stack inside it. Two details follow: - From `/refill/select`, the only screen in the nested stack, `router.dismiss()` closes the **whole modal**, because the pop bubbles to the root stack, which removes the `refill` route. - `router.canDismiss()` is `true` inside the modal as long as some stack in the focused chain has more than one screen, so a "Close" button can use it to decide whether to show a back arrow or a cross. Because these calls are relative to the **focused** chain of navigators, the same button code works whether the refill flow was opened as a modal, pushed as a normal screen, or reached from a deep link. ## Summary table | From `/refill/confirm` | Result | |---|---| | `router.back()` | `/refill/pharmacy` (stack interprets back as one pop) | | `router.dismiss()` | `/refill/pharmacy` | | `router.dismiss(2)` | `/refill/select` | | `router.dismissTo('/refill/select')` | `/refill/select` | | `router.dismissAll()` | `/refill/select` | | `router.dismissTo('/orders')` with no orders entry | current screen replaced by `/orders` | ## Picking one - **Cancel one step**: `back()` or `dismiss()`. - **Start the flow over**: `dismissAll()`. - **Jump to a known earlier step**: `dismissTo(href)`, which is robust when the step might not be in history. - **Close the whole flow** from its only screen: `dismiss()`.
- Why can router.back() and router.dismiss() behave differently on the same screen?back dispatches a generic go-back that the focused navigator interprets, so a tab navigator may switch to the previous tab. dismiss dispatches a stack pop, which tab navigators ignore, so it acts on the nearest stack instead.
- What does router.dismissTo('/orders') do if no /orders entry exists in the stack?It replaces the current screen with `/orders` rather than pushing it, so the finished step does not remain in history. When an `/orders` entry exists, it pops screens until that entry is on top.
saying these in an interview costs you the question
- router.dismiss and router.back are always interchangeable.
- dismissTo pushes the route when it is not in the history.
- dismissAll closes every navigator back to the app's root screen.
- canGoBack is reliable even before navigation has mounted.
- dismiss(2) pops two screens from a tab navigator's history.