---
title: Navigation
source: 'https://academia.sh/en/courses/react-native/navigation'
course: 'React Native'
language: en
updated: '2026-10-21T05:02:53+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains stack, tab, and drawer navigation through the concepts of route and back stack'
    - 'Measures which nodes in the view tree are mounted and unmounted when a new route is pushed and when the user goes back'
    - 'Shows why the screen below comes out kept when a new route is pushed'
    - 'Compares how keeping or dropping a hidden tab in the tree changes the fate'
---

# Navigation

Navigation arranges an app's screens as a stack, tabs, or a drawer. The lesson measures the fate of view tree nodes when a new route is pushed and when the user goes back: the screen below stays kept.

The previous lesson covered how a key that exists on only one platform sits inside shared code. From this lesson on, the course leaves single-screen examples and moves on to the layers of an app. The first layer is navigation: how the user moves from one screen to another, and what happens to the old screen along the way.

The question of the course stays the same throughout the lesson: **what does this update do to the node in the view tree?** The answer has four forms. If a node stays and its props are also the same, it is **kept**. If a node stays but a prop changes, it is **updated**. If a node first appears in the tree it is **mounted**, and if it leaves the tree it is **unmounted**. The total of the updated, mounted, and unmounted nodes is called commands: this is the work the framework has to send to the native side. A fate is never assigned by hand. The core below compares two trees, and the rule gives every node its fate.

```javascript
// fate.mjs — classifies what an update does to each node of the view tree
export const FATES = ["kept", "updated", "mounted", "unmounted"];

export function el(type, props = {}, children = [], key = null) {
  return { type, props, children, key };
}

function segment(node) {
  return node.key === null ? node.type : `${node.type}[${node.key}]`;
}

function identity(node, index) {
  return node.key === null ? `#${index}` : `key:${node.key}`;
}

function sameProps(a, b) {
  const names = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
  return names.every((n) => JSON.stringify(a[n]) === JSON.stringify(b[n]));
}

function whole(node, path, fate, ops) {
  const here = `${path}/${segment(node)}`;
  ops.push({ fate, path: here });
  node.children.forEach((child) => whole(child, here, fate, ops));
}

export function reconcile(prev, next, path = "", ops = []) {
  if (prev === null) {
    whole(next, path, "mounted", ops);
    return ops;
  }
  if (next === null) {
    whole(prev, path, "unmounted", ops);
    return ops;
  }
  if (prev.type !== next.type || prev.key !== next.key) {
    whole(prev, path, "unmounted", ops);
    whole(next, path, "mounted", ops);
    return ops;
  }
  const here = `${path}/${segment(next)}`;
  ops.push({ fate: sameProps(prev.props, next.props) ? "kept" : "updated", path: here });
  const before = new Map(prev.children.map((child, i) => [identity(child, i), child]));
  const matched = new Set();
  next.children.forEach((child, i) => {
    const id = identity(child, i);
    matched.add(id);
    reconcile(before.has(id) ? before.get(id) : null, child, here, ops);
  });
  prev.children.forEach((child, i) => {
    if (!matched.has(identity(child, i))) reconcile(child, null, here, ops);
  });
  return ops;
}

export function printOps(ops) {
  ops.forEach((op) => console.log(`${op.fate}: ${op.path}`));
}

export function countFates(ops) {
  for (const name of FATES) {
    console.log(`${name}: ${ops.filter((op) => op.fate === name).length}`);
  }
  console.log(`commands: ${ops.filter((op) => op.fate !== "kept").length}`);
}
```

The rule says a subtree is mounted if it is only in the next tree and unmounted if it is only in the previous one. If the type or the key differs, the whole old node is unmounted and the whole new one is mounted. Otherwise it compares the props and matches the children by key or by position. The model builds this fate's rule; the model is not the framework itself.

## Stack: Pushing a Route and Going Back

The most common form of navigation is the stack. Each screen corresponds to a **route**: the route is the name of the screen. The ordered list of open routes is called the **back stack**. When the user taps a row, a new route is pushed onto the end of the stack; when the user presses the back button, the last route is popped. A stack navigation library keeps this list and builds one screen for each route. The app author's job is to define the routes and which screens they lead to.

```jsx
// Platform code: RootStack
function RootStack() {
  return (
    <Stack>
      <Screen name="list" component={ListScreen} />
      <Screen name="detail" component={DetailScreen} />
    </Stack>
  );
}
```

This fragment is not measured; the model does not run the library. In the model the stack is a list of route names, and `renderStack` turns each route into a `Screen` node keyed by the route name. The key matters: a route with the same name counts as the same screen in both trees.

**AL1.** The model keeps the screen of every route in the stack in the view tree; even when the route on top covers the one below, the lower screen's node does not leave the tree. How a real library makes this choice depends on the library; the model makes a choice and says so openly.

```javascript
// stack.mjs — builds the view tree from the navigation stack's routes
import { el } from "./fate.mjs";

export function renderStack(routes) {
  return el("Stack", {}, routes.map((name) =>
    el("Screen", { title: name }, [el("Text", { text: name })], name)));
}
```

First we are on the list screen and tap a row. The stack goes from the route `list` to the routes `list`, `detail`.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderStack } from "./stack.mjs";

const ops = reconcile(renderStack(["list"]), renderStack(["list", "detail"]));
printOps(ops);
countFates(ops);
```

```text
kept: /Stack
kept: /Stack/Screen[list]
kept: /Stack/Screen[list]/Text
mounted: /Stack/Screen[detail]
mounted: /Stack/Screen[detail]/Text
kept: 3
updated: 0
mounted: 2
unmounted: 0
commands: 2
```

When the new route is pushed, the `detail` screen and the text inside it are mounted: two nodes. The `/Stack/Screen[list]` below stays kept, because its props did not change and its key stayed the same. The command count includes only the nodes of the new screen. The rule does not look at how many screens the app has, but at which node changed in this update.

Going back is the reverse of the same rule. One route is popped from a three-screen stack; the focus line, that is, the screen returned to, is printed separately.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderStack } from "./stack.mjs";

const ops = reconcile(
  renderStack(["list", "detail", "note"]),
  renderStack(["list", "detail"]),
);
printOps(ops.filter((op) => op.path === "/Stack/Screen[detail]"));
countFates(ops);
```

```text
kept: /Stack/Screen[detail]
kept: 5
updated: 0
mounted: 0
unmounted: 2
commands: 2
```

The `detail` screen that is returned to is kept; the `note` screen that was popped and its text are unmounted. The state of an unmounted screen, that is, an entry typed into it or a scroll position, disappears together with the node. When the user goes back and then navigates to the same route again, the screen is built from scratch. For this reason, information that must not be lost when a screen closes lives outside the screen, in the layers covered in later lessons.

## Replacing a Route

Navigation also has replacing: the last route of the stack is popped and another route is put in its place. Moving from one product page to another product page is an example. The code of the screen is the same; only which product it shows changes. Which product is shown enters the route as a parameter.

**AL3.** The model merges the route name and its parameter into a single key: `product-1` and `product-2` are two separate routes. Real libraries usually build the key from the route name and an identifier; the model reduces this combination to a single string.

```javascript
// main.mjs
import { reconcile, printOps } from "./fate.mjs";
import { renderStack } from "./stack.mjs";

printOps(reconcile(renderStack(["list", "product-1"]), renderStack(["list", "product-2"])));
```

```text
kept: /Stack
kept: /Stack/Screen[list]
kept: /Stack/Screen[list]/Text
mounted: /Stack/Screen[product-2]
mounted: /Stack/Screen[product-2]/Text
unmounted: /Stack/Screen[product-1]
unmounted: /Stack/Screen[product-1]/Text
```

Because the key differs, the `product-1` screen is unmounted together with its text and the `product-2` screen is mounted together with its text; `list` is kept. Even though the screen type is the same, the rule matches nodes by key, so two product pages do not count as the same node. The result reaches the user like this: a scrolled position or a note typed on the first product page does not carry over to the second product. If carrying it over is wanted, the identity of the screen becomes the route name and the product information enters as a prop; in that case the same node would have been updated. Which one is right depends on the app feature, and the rule does not make this decision.

## Tab and Drawer

In tab navigation the routes do not form a stack, they are siblings: the user picks one from a tab bar and only the screen of that tab is visible. Drawer navigation is the same idea opened from the edge; the routes are siblings, and a menu makes the selection instead of a bar. The three differ as navigation in the back stack; for the view tree the real question is another: does the screen of a hidden tab stay in the tree?

**AL2.** The model measures this question with two options. In one, the hidden tabs stay in the tree and only a visibility prop changes. In the other option only the selected tab is in the tree. Real libraries offer one of these two behaviors by default, and most allow changing it as a setting; the model does not say which one is the default.

```javascript
// tabs.mjs — builds the tab bar's view tree; the keeping option holds the hidden tab in the tree
import { el } from "./fate.mjs";

export function renderTabs(active, names, keepHidden) {
  const shown = keepHidden ? names : names.filter((name) => name === active);
  return el("Tabs", {}, shown.map((name) =>
    el("Screen", { visible: name === active }, [el("Text", { text: name })], name)));
}
```

The user moves from the `home` tab to the `search` tab. Both options go through the same transition, and only the fate counts are compared.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderTabs } from "./tabs.mjs";

const names = ["home", "search", "profile"];
console.log("keeping:");
countFates(reconcile(renderTabs("home", names, true), renderTabs("search", names, true)));
console.log("discarding:");
countFates(reconcile(renderTabs("home", names, false), renderTabs("search", names, false)));
```

```text
keeping:
kept: 5
updated: 2
mounted: 0
unmounted: 0
commands: 2
discarding:
kept: 1
updated: 0
mounted: 2
unmounted: 2
commands: 4
```

In the keeping option two screens are updated: one becomes visible, the other becomes invisible. The text nodes and the third tab are kept. In the discarding option the `home` screen is unmounted and the `search` screen is mounted; each is two nodes with its text. The same transition gives two different fate distributions in the two options. The keeping option preserves state when the user returns to the tab, but it carries the cost of screens that are not visible in the tree. The discarding option keeps the tree small and builds the screen from scratch on return.

## Reading the Source Fragment Line by Line

The stack definition above is short, but it carries four separate decisions. The first decision is the `Stack` container itself: the screens are defined as children of a stack, so the relationship between them is order. The second decision is the `name` value in each `Screen` line. This name is the identity of the route; what turns into a key in the tree is also this. The third decision is the `component` line: a route is not merely a name, it is the definition of the screen that will be opened under that name. The fourth decision is the order of the definitions: the route defined first becomes the starting screen, and the others can be reached only through a transition.

None of these lines mounts a node. The definition says under which names screens can exist; it does not say which one is open right now. What is open is the runtime content of the stack: the route list. The tree derives from this list, and the rule classifies what the tree does when the list changes. This is why changing the definition and changing the stack are different jobs: the first affects the code of the app, the second affects the user's current path.

## As the Stack Deepens

The user goes from a list to a detail, and from the detail to a sub-detail. At each step a route is added to the stack. In this measurement the push left the lower screens kept and mounted only the nodes of the new screen. This makes navigation look cheap: the cost of a step is bounded by the size of the new screen, not by the length of the stack.

But the same result has a price. Because the lower screens stay in the tree, each of them keeps carrying its nodes, its state, and its native view. As the stack deepens, the tree grows. The user finding the place where they left off on returning is the gain of this choice; the growth of the tree is its price. If an app allows very deep navigation, it has to pay the price of the gain knowingly. The question to ask when designing navigation is as much "what state should be there when the user goes back?" as "can the user go back?".

Replacing a route changes this balance. In the replacement measurement the old screen was unmounted, the new one was mounted, and the state of the old screen disappeared. If the user moves from the detail of one product to the detail of another, this is often the wanted behavior: it makes no sense for two products to share a scroll position. The same behavior is a loss when the user wants to return to the same product. The fate does not say which behavior is right; the fate says only which one happens.

## The Bounding Measurement

In the stack, the lower screen coming out kept does not come from navigation itself but from an assumption of the model: AL1 does not write that the lower screen is covered into its props. If a library carried a prop such as `onTop` to the lower screen, the same push would give a different fate. Below, the same push is measured with two ways of building the tree.

```javascript
// main.mjs
import { el, reconcile, printOps, countFates } from "./fate.mjs";
import { renderStack } from "./stack.mjs";

function renderMarked(routes) {
  return el("Stack", {}, routes.map((name, i) =>
    el("Screen", { onTop: i === routes.length - 1 }, [el("Text", { text: name })], name)));
}

const focus = (op) => op.path === "/Stack/Screen[list]";
const plain = reconcile(renderStack(["list"]), renderStack(["list", "detail"]));
const marked = reconcile(renderMarked(["list"]), renderMarked(["list", "detail"]));
console.log("unmarked:");
printOps(plain.filter(focus));
console.log("marked:");
printOps(marked.filter(focus));
countFates(marked);
```

```text
unmarked:
kept: /Stack/Screen[list]
marked:
updated: /Stack/Screen[list]
kept: 2
updated: 1
mounted: 2
unmounted: 0
commands: 3
```

The same push made the `/Stack/Screen[list]` node kept or updated depending on how the tree was built. The rule does not see that the screen is covered; it sees only that a prop in the tree changed. If the information about being covered is written into the tree as a prop, every push produces a command for the lower screen. So how the library reports being covered is a part of the navigation cost, and the rule does not make this decision; the lesson says it in prose.

## Common Misconceptions

- "Going back builds the previous screen again." In the going-back measurement the nodes of the previous screen came out kept; only the screen of the popped route was unmounted. Because nothing was built again, the scroll position also stays in place.
- "Any screen in the stack can be removed from the tree, and the user will not notice." If the lower screen is removed from the tree, its state goes with it. When the user goes back, the screen is built from scratch and unfinished work is lost.
- "The route name is enough as an identity." If the same name can be opened twice with different parameters, the name alone cannot be the key; the model therefore makes the name and the parameter a key together. If the key does not match, the same screen counts as a different screen and is unmounted and built again.
- "Tab and stack are the same transition." In a stack a new route is added and a way back is formed. In tabs the routes are siblings, and the transition between them produces the fate of whichever of the two options above was built.

## Summary

- This lesson's fate: kept. When a new route was pushed, the `/Stack/Screen[list]` node came out kept, because its props and its key did not change.
- Pushing a route mounts the nodes of the new screen; going back unmounts the nodes of the popped screen and so also destroys that screen's state.
- A tab switch produces updated, or unmounted together with mounted, depending on whether the hidden tab stays in the tree.
- The lower screen being kept depends on the assumption that being covered is not written into the tree as a prop; if it is written, the same node becomes updated.
- The rule does not see the inner workings of the navigation library; it only compares two trees.

## Next Step

In this lesson navigation determined at which nodes screens are mounted and unmounted. Information shared between screens is a separate question: when a cart count changes on one screen, a badge on another screen must change too. The next lesson, `State Management`, measures which node a single change updates and which node it leaves kept.
