skip to content

In React Navigation 7, why do two headers appear when navigators are nested, and how does getFocusedRouteNameFromRoute help title the remaining one?

level: middleimportance: should knowfreq 45%

answer

  1. both navigators draw headers
  2. headerShown: false on one level
  3. title the parent from the child
  4. undefined before the child initialises
  5. default to the initial route name

basics

~20 s

The parent and the nested navigator each render a header by default, so two stack up. Hide one level with headerShown: false; if the parent's header stays, getFocusedRouteNameFromRoute(route) in its options reads the focused child's name.

solid answer

~50 s

In React Navigation 7, headers belong to navigators: the native stack and the JavaScript bottom tabs both default `headerShown` to `true`. Nest a stack in the Profile tab, or put the tabs in a root stack, and each level draws its own header, one above the other. Fix it by hiding one level with `headerShown: false`, usually on the parent screen that renders the nested navigator, so the child's header, which knows its screens, remains. When you keep the parent's header instead, for example the root stack's header above the tabs, it needs the title of the focused child. `getFocusedRouteNameFromRoute(route)` reads the nested navigator's state from the parent screen's route inside an `options` callback. It returns `undefined` until the nested navigator has initialised, unless a `screen` param names the child, so default to the initial route name.

go deeper

for a junior

Recall that each navigator draws its own header, so nesting stacks two of them, and headerShown: false on one level removes the duplicate.

for a middle

Explain which level to hide in each layout and how getFocusedRouteNameFromRoute titles a parent header from the nested focused route, with its undefined first render.

for a senior

Show you choose header ownership per layout, avoid focused-route tricks for hiding the tab bar, and reach parents with getParent where options must flow upward.

for a principal

Set a header ownership convention across the navigation tree so teams adding nested navigators do not reintroduce doubled or mistitled headers.

## Headers belong to navigators In **React Navigation 7**, a header is rendered by the **navigator**, for each of its screens, not by the screen component. Two navigators used for most apps both show one by default: - the native stack, where `headerShown` defaults to `true`; - the JavaScript bottom tab navigator, whose `headerShown` option is documented as defaulting to `true`. When navigators are nested, every level renders its own header for its own screen, and they stack vertically. Interviewers use this to check that a candidate knows headers come from navigators. ## The two classic cases In a dating app: 1. **Stack inside a tab.** The `Profile` tab renders a stack (`ProfileHome → Settings`). The tab navigator draws a header titled `Profile`, and the nested stack draws a header titled `Settings` below it. 2. **Tabs inside a stack.** The root stack's `MainTabs` screen renders the tabs. The root stack draws a header titled `MainTabs`, and the tab navigator draws a header for `Discover` below it. ## Fixing the doubled header Pick the level that should own the header for those screens and hide the other with `headerShown: false`: | Layout | Usually hide | Keep | Why | |---|---|---|---| | Stack inside a tab | the tab's header for that tab | the nested stack's header | it has a back button and the real screen title | | Tabs inside a stack | the root stack's header on `MainTabs` | each tab's header | each tab titles itself | | Tabs inside a stack, one shared header wanted | the tabs' headers | the root stack's header | title it from the focused tab | The first two rows need nothing more. The third needs the parent to know which child is focused. ## getFocusedRouteNameFromRoute `getFocusedRouteNameFromRoute(route)` takes the **route object of the screen that renders a nested navigator** and returns the name of the focused screen inside it. It is used in that screen's `options` callback in the parent navigator: ```tsx <RootStack.Screen name="MainTabs" component={MainTabs} options={({ route }) => ({ headerTitle: getFocusedRouteNameFromRoute(route) ?? 'Discover', })} /> ``` How it decides, per its source: - if the nested navigator's state is available on the route, it returns the focused route's name, using the state's `index`, or for a partial state without an index the first route for tabs and the last route for a stack; - if there is no state yet but the route's params contain a `screen` string, it returns that; - otherwise it returns **`undefined`**, which is the case on the first render before the nested navigator has initialised its state. That last case is why the `?? 'Discover'` fallback is required: default to the nested navigator's initial route name. ## Hiding the right level in code For the stack inside the Profile tab, the header switch goes on the **tab's screen**, not inside the stack: ```tsx <Tab.Screen name="Profile" component={ProfileStack} options={{ headerShown: false }} /> ``` The other tabs, `Discover` and `Matches`, keep their tab headers if they render plain screens. If every tab renders its own stack, set `headerShown: false` once in the tab navigator's `screenOptions` instead of per tab. For tabs inside the root stack, the equivalent is `options={{ headerShown: false }}` on the root stack's `MainTabs` screen. Either way exactly one navigator draws the header for any visible screen. ## Limits and alternatives - It reads the nested state from the **parent screen's route**; calling it with a route from inside the child navigator returns that child's own nested state, if any, not its siblings'. - It only helps where the parent needs to react to the child in **options**. A nested screen that wants to change something on its parent can call `navigation.getParent()?.setOptions(...)` instead. - Using it to hide the tab bar for certain nested screens works but tends to glitch during transitions; moving those screens into the root stack is the structural fix. **Quick checklist:** - Two headers stacked: find the two navigators, hide one level. - Title of the parent header wrong or stuck on the parent screen name: use `getFocusedRouteNameFromRoute` in the parent's `options`, with a default. - Title flashes the default on launch: expected, because the nested state does not exist on the first render.

  • Why does getFocusedRouteNameFromRoute return undefined on the first render?
    It reads the nested navigator's state from the parent screen's route. On the first render that child navigator has not initialised its state yet, so unless a `screen` param names the target, there is nothing to read. Default to the nested navigator's initial route name, for example `?? 'Discover'`.
  • In a stack-inside-tab layout, why hide the tab's header rather than the nested stack's header?
    The nested stack's header knows each pushed screen's title and shows the back button for that stack. Hiding the tab navigator's header for that tab leaves one header that matches the screen being shown; hiding the stack's header would leave a static tab title and no back button.

Two nested navigators with headers are like a book whose every chapter also prints the book's title above the chapter title; you keep one title line, and if it is the book's line, you have to look inside to learn which chapter is open.

saying these in an interview costs you the question

  • Two headers mean a screen component rendered a header twice.
  • getFocusedRouteNameFromRoute always returns the focused screen name, even on first render.
  • getFocusedRouteNameFromRoute is called inside the nested screen with its own route.
  • Headers are shared across nested navigators, so only one can ever show.
  • Hiding the nested stack's header is the usual fix for a stack inside a tab.