skip to content

With Expo Router, how does Stack.Protected's guard prop keep a streaming app's account screens from signed-out users, and where does a blocked user land?

level: middleimportance: should knowfreq 38%

answer

  1. route list changes, not a redirect
  2. guard false: screens left out
  3. anchor route, else first available
  4. history entries dropped on flip
  5. nested guards must all be true

basics

~20 s

Screens wrapped in <Stack.Protected guard={isSignedIn}> are left out of the navigator while the guard is false. A deep link to one lands on the anchor route or first available screen, and a guard flipping false removes their history entries.

solid answer

~40 s

In Expo Router you wrap the account screens in `<Stack.Protected guard={isSignedIn}>`. While `guard` is false, those screen names are **left out of the navigator's route list**, so nothing can render them; it is not a mount-then-redirect. A cold start or deep link to a guarded path shows the stack's **anchor route** (`unstable_settings.anchor`, usually `index`), or the first available screen if the anchor is guarded too. If the guard turns false while the user is on a guarded screen, **all of its history entries are removed**, so back cannot return to it. Nested `Protected` blocks need every guard true, each screen may be declared once, `Tabs.Protected` hides the tab button, and none of this replaces server-side authorization.

code

tsx · 31 lines
tsx
// src/app/_layout.tsx
import { Stack } from 'expo-router';
import { SessionProvider, useSession } from '../session';

export default function RootLayout() {
  return (
    <SessionProvider>
      <RootNavigator />
    </SessionProvider>
  );
}

function RootNavigator() {
  const { session, isAdmin } = useSession();
  const isSignedIn = !!session;

  return (
    <Stack>
      <Stack.Screen name="index" />
      <Stack.Protected guard={isSignedIn}>
        <Stack.Screen name="account" />
        <Stack.Protected guard={isAdmin}>
          <Stack.Screen name="studio" />
        </Stack.Protected>
      </Stack.Protected>
      <Stack.Protected guard={!isSignedIn}>
        <Stack.Screen name="sign-in" />
      </Stack.Protected>
    </Stack>
  );
}

go deeper

for a junior

Recall the shape: wrap the screens in Stack.Protected inside the layout and pass a boolean guard computed from whether a session exists.

for a middle

Explain that a false guard removes screen names from the navigator's route list, where deep links fall back, and why history entries vanish on sign-out.

for a senior

Show the production edges: declare each screen once, nest guards for roles, keep one source of truth for the guard, and still enforce authorization on the API.

for a principal

Frame guards as client-side routing policy: weigh how roles map onto nested groups, what the web build exposes, and where the real access-control boundary must live.

## What `Stack.Protected` is In **Expo Router** (the file-system router built on React Navigation), every file under `src/app` is a route that exists from the moment the app starts. **Protected routes** are how a layout takes some of those routes away at runtime. You wrap one or more `Stack.Screen` declarations in `<Stack.Protected guard={...}>`, where `guard` is a plain boolean computed from your app state, usually "is there a session". While the guard is `true`, the wrapped screens behave like any other screen. While it is `false`, Expo Router **leaves those screen names out of the navigator's route list**. The navigator simply does not know they exist, so it cannot render them. This is a different mechanism from a redirect: nothing mounts first and then bounces the user away. For a streaming app, the root layout might look like this: ```tsx <Stack> <Stack.Screen name="index" /> <Stack.Protected guard={isSignedIn}> <Stack.Screen name="account" /> </Stack.Protected> <Stack.Protected guard={!isSignedIn}> <Stack.Screen name="sign-in" /> </Stack.Protected> </Stack> ``` The `account` folder (profile, billing, downloads) is invisible to signed-out users, and `sign-in` disappears once they have a session. ## Where a blocked user lands What happens depends on how the user meets the guarded screen: 1. **Cold start or deep link to a guarded path** (for example `/account/billing` while signed out): the restored state is missing that route, so the stack shows its **anchor route** instead: the route named by `unstable_settings.anchor`, which in most apps is `index`. Without an anchor, or if the anchor is itself guarded out, the navigator starts at the first screen that is still available. 2. **The guard turns false while the screen is on the stack** (sign-out from the billing screen): every history entry for the guarded screens is **removed**, so the user lands on whatever remains beneath them. If nothing remains, the stack starts again at the anchor, or at the first available screen. 3. **An in-app `Link` or `router` call to a guarded screen**: the screen is not in the navigator's list, so it is never shown. The history removal in case 2 is the important production property: after sign-out, the Android back button or an iOS back swipe cannot return to the billing screen, because that entry no longer exists. | Situation | Result | |---|---| | Deep link to guarded route | Anchor route, else first available screen | | Guard flips false while shown | Its history entries dropped; remaining stack shown | | Stack empty after removal | Anchor route, else first available screen | | Guard flips true | Screen becomes reachable; nothing navigates automatically | ## Rules that trip people up - **Declare each screen once.** A screen name may appear in only one place in a layout. Putting `account` both inside and outside a `Stack.Protected` throws a "Screen names must be unique" error in development. If availability depends on logic, express the logic in the guard. - **Nested guards combine.** A `Stack.Protected` inside another one is reachable only when **both** guards are true, which is how an admin area inside the signed-in area works. - **Undeclared routes are not protected.** Expo Router includes every file route by default; only screens you list inside a `Protected` wrapper are guarded. A new file dropped into `src/app` without a declaration is reachable. - **Turning a guard on does not navigate.** It only makes the screen reachable. Navigating there is still your code's job (or happens automatically when the screen the user is on becomes guarded out). - **Guard the folder, not every file.** Guarding the `account` route covers the nested `account/_layout.tsx` and all of its children. ## Tabs and Drawer `Tabs.Protected` does the same job for a tab navigator, and Drawer has an equivalent. In a tab bar the effect is visible: a guarded-out tab's **button disappears** from the bar, so a signed-out viewer of the streaming app never sees a "Downloads" tab at all. Custom navigators built with `withLayoutContext` receive `Protected` as well. ## What it is not Protected routes are **client-side navigation rules**, not access control. They decide which screens the router will show; they do not protect data. - The API behind the account screens must still check the session on every request. - On web static output, Expo Router generates no HTML for protected routes, but anyone who knows the URLs can still request the page or JavaScript files directly. - The guard is only as trustworthy as the state it reads. A guard computed from a stale cached flag shows stale screens. Protected routes arrived in Expo Router 5 (Expo SDK 53) for stacks, with `Tabs.Protected` following in 5.1. In the Expo SDK 57 target (Expo Router 57.x), they are the documented authentication pattern; the older one placed a redirect in a group layout, which mounted the layout first and then navigated away.

  • Does Stack.Protected stop a signed-out user from reading account data?
    No. It is a client-side navigation rule: it decides which screens the router will render. The API that serves profile and billing data must still verify the session on every request. On web static output, Expo Router generates no HTML for protected routes, but someone who knows the URLs can still fetch the page or JavaScript files directly, so the server remains the real boundary.
  • How would you hide a Downloads tab from signed-out viewers in an Expo Router tab layout?
    Wrap that tab's screen in `<Tabs.Protected guard={isSignedIn}>` inside the `Tabs` layout. While the guard is false the route is left out of the tab navigator, so its button disappears from the tab bar and navigation to it does not show it. When the guard turns true, the tab appears; declare the screen only inside the protected block, never a second time outside it.
  • A signed-in viewer on the billing screen taps Sign out. What does the stack look like afterwards?
    Clearing the session flips the account guard to false, so every history entry for the guarded account screens is removed from the stack. The user sees whatever screen remains beneath them, such as the home screen. If nothing remains, the stack restarts at its anchor route or the first available screen, and a back gesture cannot return to billing.

saying these in an interview costs you the question

  • Stack.Protected renders the screen, then redirects away in an effect
  • Declaring the same screen inside and outside Stack.Protected is fine
  • A nested Protected block is reachable if either guard is true
  • Setting guard to true navigates the user to the protected screen
  • Protected routes secure the account data on the server
  • After sign-out the back button can still reach the billing screen