skip to content

In an audiobook app using React Navigation 7 bottom tabs, how do lazy, freezeOnBlur, popToTopOnBlur and detachInactiveScreens change how inactive tabs behave?

level: seniorimportance: nice to knowfreq 22%

answer

  1. render on first visit, then stay mounted
  2. freeze re-renders of unfocused tabs
  3. reset a tab's nested stack on blur
  4. detach native views to save memory
  5. all four default to keeping state

basics

~20 s

lazy (default true) renders a tab on first visit; freezeOnBlur (default false) stops unfocused tabs from re-rendering; popToTopOnBlur (default false) resets a tab's nested stack when leaving it; detachInactiveScreens detaches inactive tabs' native views to save memory.

solid answer

~50 s

Bottom tab screens stay mounted after their first visit, which preserves state but means an unfocused Library tab still re-renders when a store it subscribes to changes, for example on every playback-progress tick. `lazy` (default `true`) only controls the first render: a tab is rendered when first focused or preloaded, and `false` renders all tabs up front. `freezeOnBlur` (default `false`, or `true` after `enableFreeze()` from `react-native-screens`) suspends re-rendering of unfocused tabs until they are focused again, iOS and Android only. `popToTopOnBlur` (default `false`) pops a stack nested in the tab back to its first screen when the user leaves the tab. `detachInactiveScreens`, a navigator-level prop that in the current source defaults to `true` on iOS, Android and web, detaches the native views of inactive screens to save memory without unmounting their React state. Profile before and after; freezing hides work but does not remove it.

code

tsx · 23 lines
tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Text } from 'react-native';

function HomeScreen() {
  return <Text>Continue listening</Text>;
}
function SearchScreen() {
  return <Text>Search</Text>;
}
function LibraryScreen() {
  return <Text>Library</Text>;
}

export const RootTabs = createBottomTabNavigator({
  screenOptions: {
    freezeOnBlur: true,
  },
  screens: {
    Home: HomeScreen,
    Search: { screen: SearchScreen, options: { lazy: false } },
    Library: { screen: LibraryScreen, options: { popToTopOnBlur: true } },
  },
});

go deeper

for a junior

Recall that tabs render on first visit and then stay mounted, which is why their state survives switching.

for a middle

Explain what each of lazy, freezeOnBlur, popToTopOnBlur and detachInactiveScreens does, with their defaults and the react-native-screens requirement for freezing.

for a senior

Diagnose background re-renders and memory growth in tabs, apply freezeOnBlur and selective subscriptions, and separate product decisions like popToTopOnBlur from performance fixes.

for a principal

Decide how the app balances instant tab switching, memory on low-end devices and state retention, and document the defaults every tab should use.

## The default: mounted, visible only when focused A React Navigation 7 bottom tab navigator renders a tab's screen the first time it is focused, then **keeps it mounted**. That is what preserves the Search tab's query and the Library tab's scroll position. The cost is that mounted but unfocused screens are still React components: if the Home tab subscribes to a playback-progress store that updates every second, it keeps re-rendering while the user browses Library. Four options tune this behaviour. | Option | Where | Default | Effect | |---|---|---|---| | `lazy` | Screen option | `true` | Render a tab only when first focused or preloaded | | `freezeOnBlur` | Screen option | `false` (or `true` after `enableFreeze()`) | Suspend re-renders of the tab while unfocused | | `popToTopOnBlur` | Screen option | `false` | Pop a stack nested in the tab back to its first screen when leaving | | `detachInactiveScreens` | Navigator prop | `true` on iOS, Android and web in the current source | Detach inactive screens' native views to save memory | ## `lazy` With the default `lazy: true`, a tab is not rendered until the user first navigates to it, unless it was preloaded. That keeps startup cheap: the Library tab's large grid of covers is not built until needed. `lazy: false` renders the tab on the navigator's first render, which is useful when a tab must be ready instantly and its first render is expensive, at the cost of slower startup. `lazy` has **no effect after the first visit**; it does not unmount anything. ## `freezeOnBlur` `freezeOnBlur` uses `react-native-screens`' freeze support to **suspend re-rendering** of a screen while it is not focused. Updates that would re-render it are held until it becomes focused again, when it catches up. - It defaults to `false`, or to `true` when `enableFreeze()` from `react-native-screens` is called at the top of the app. - It needs `react-native-screens` 3.16 or newer and works only on iOS and Android. - It is the targeted fix for "background tabs re-render on every playback tick". Freezing is documented as suspending re-renders; it is not a way to stop work. Timers, polling and subscriptions that should pause while a tab is hidden belong in focus-aware logic in the screen. ## `popToTopOnBlur` When a tab contains a stack, for example Library, then a book's details, then its chapters, leaving the tab normally preserves that position. `popToTopOnBlur: true` pops the nested stack back to its first screen whenever the user switches away, so returning to Library starts at the library root. It defaults to `false`. Choose it when product wants each tab visit to start fresh; leave it off when users expect to resume where they were. ## `detachInactiveScreens` `detachInactiveScreens` is a navigator-level prop. When `true`, inactive screens are detached from the native view hierarchy to save memory; their React state stays. The option's comment says it defaults to `true` on Android, while the current source defaults it to `true` on web, Android and iOS. Turning it off keeps inactive views attached, which can help when a background tab must keep drawing, at a memory cost. ## A practical tuning order 1. **Measure**: confirm with a profiler that unfocused tabs re-render or that memory grows. 2. **Fix the subscription first**: select only the state the screen needs, so a progress tick does not re-render a whole tab. 3. **Enable `freezeOnBlur`** on heavy tabs that do not need to update while hidden. 4. **Keep `lazy: true`** unless a tab must be instantly ready. 5. **Use `popToTopOnBlur`** only as a product decision about navigation history, not as a performance tool. ## Common misconceptions - **"`lazy` unmounts tabs when you leave them."** It only delays the first render. - **"Frozen tabs are unmounted."** Their state is intact; rendering is paused. - **"Detaching views loses state."** Detachment is a native-view optimisation; React state remains.

  • Should you rely on freezeOnBlur to stop polling or a playback-progress timer in a background tab?
    No. `freezeOnBlur` is documented as suspending re-renders of inactive screens; it is not a mechanism for stopping work. Timers, polling and subscriptions that should run only while a tab is visible need focus-aware logic in the screen itself, which is a separate lifecycle concern.
  • When would you set lazy: false on a tab?
    When a tab must appear instantly on first tap and its first render is expensive, such as a search screen that builds indexes or a heavy list. Rendering it with the navigator trades a slower initial load for no delay on first focus.

saying these in an interview costs you the question

  • lazy: true unmounts a tab when the user leaves it.
  • freezeOnBlur unmounts the screen and restores it from saved state.
  • popToTopOnBlur is a performance option that reduces memory.
  • detachInactiveScreens resets the React state of inactive tabs.
  • freezeOnBlur is the right way to stop polling and timers in a background tab.