---
title: 'React Native'
source: 'https://academia.sh/en/courses/react-native'
language: en
updated: '2026-10-21T05:02:53+00:00'
license: 'CC BY-SA 4.0'
lessons: 20
outcomes:
    - 'Builds interfaces with core components and the style system'
    - 'Improves list performance by measuring it'
    - 'Reaches platform capabilities through native modules'
---

# React Native

React Native is a bridge-based framework in which shared JavaScript code describes a declarative view tree, and every update reaches the native views as commands. The course measures every topic with a single question — what does this update do to the node in the view tree? A node is kept, updated, mounted or unmounted; core components, the style system, navigation, state, data and storage layers, list performance, profiling, testing, native modules and release all pass through the same frame.

## Core Structure

React Native's working model, environment setup, core components, the style system and flexbox layout, touch and interaction, platform-specific code.

### What Is React Native

Source: https://academia.sh/en/courses/react-native/what-is-react-native

In a bridge-based framework, shared code does not draw the screen; it describes a view tree. The lesson classifies what an update does to each node of the tree with four fates: kept, updated, mounted, unmounted.

The previous course classified the four paths of cross-platform development and introduced the bridge-based approach: shared code maps a UI node to the platform's own view and reaches the native side through a bridge crossing. This course opens up that approach in a single framework that works with a declarative component model. The framework's name appears only in this lesson's title and in a few places; the lesson is about a class of concepts, not about the framework.

Shared code does not draw the screen directly. On every update it describes how the screen should look as a **view tree**. The framework compares the new tree with the previous one and sends commands to the native side only for the nodes that changed. The course's question comes from here: **what does this update do to the node in the view tree?**

The answer takes four forms, and their names do not change throughout the course. If the node stands in the same place in both trees with the same type and the same key, and its props are equal, the update leaves the node **kept**, and no command goes to the native side. If the node stands in the same place with the same type but at least one prop has changed, the node is **updated**: it stays where it is and its native view is updated. If the node has no counterpart in the previous tree, it is **mounted** and its native view is created from scratch. If the node has no counterpart in the next tree, it is **unmounted**, and the state inside it disappears along with its native view. The shared name for the four answers is **fate**. In the output they are written in ASCII: `kept`, `updated`, `mounted`, `unmounted`.

#### Describing the Screen

In a declarative UI, code does not say "create this view, then update that one"; given a state, it returns the screen as it should look in that state. The snippet below shows the shape of such functions. The snippet is not measured; the model does not run the framework's code.

```jsx
// Platform code: Orders
function Orders({ orders, loading }) {
  return (
    <View>
      <Text>{orders.length} orders</Text>
      {loading ? <ActivityIndicator /> : null}
    </View>
  );
}
```

The same description runs on two platforms; each platform turns the nodes in the description into its own native views. On iOS and on Android, what appears on the screen is the platform's own control, not a picture drawn by shared code. All shared code holds is the description.

What the function returns is not a view but a description: a container, a text inside it, and depending on the state a loading indicator. The framework takes this description, places it side by side with the previous one, and passes the difference to the native side as commands. A command is a unit of work that crosses the bridge: "mount this view", "change this prop of that view", "unmount this view". Regenerating the tree is a render, but it is not a command by itself. If the new tree comes out equal to the previous one, nothing crosses the bridge.

A note on vocabulary. The word **prop** here means a value a node receives from outside, for example the text of a text node. Throughout the course, "prop" stays in this sense; when a capability of the product is meant, it is called an "app feature".

#### The Core

The rule gives the fate, not a label. In the core below, `el` builds a node: a type, props, children, and an optional key. `reconcile` compares two trees and produces a `{ fate, path }` record for each node. `printOps` prints the records, `countFates` counts the four fates in a fixed order and gives the command total on the last line. The rule asks in order: if there is no previous node, the node and its subtree are mounted; if there is no next node, it is unmounted; if the type or key differs, the old subtree is unmounted, the new one is mounted, and the props are not examined; if the type and key are the same, the props are compared by content, and if they are equal the node is kept, otherwise updated. Then the children are matched: a child with a key by its key, a child without a key by its position.

```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}`);
}
```

#### Eight Nodes of One Update

The first measurement is a single update of an orders screen. The fourth order arrives and the loading indicator goes away. In this measurement the tree is built directly with `el`.

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

const before = el("Screen", {}, [
  el("Header", { title: "Orders" }),
  el("Text", { value: "3 orders" }),
  el("List", {}, [
    el("Row", { name: "Coffee" }, [], "a"),
    el("Row", { name: "Tea" }, [], "b"),
  ]),
  el("Spinner", { visible: true }),
]);

const after = el("Screen", {}, [
  el("Header", { title: "Orders" }),
  el("Text", { value: "4 orders" }),
  el("List", {}, [
    el("Row", { name: "Coffee" }, [], "a"),
    el("Row", { name: "Tea" }, [], "b"),
    el("Row", { name: "Water" }, [], "c"),
  ]),
]);

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Screen
kept: /Screen/Header
updated: /Screen/Text
kept: /Screen/List
kept: /Screen/List/Row[a]
kept: /Screen/List/Row[b]
mounted: /Screen/List/Row[c]
unmounted: /Screen/Spinner
kept: 5
updated: 1
mounted: 1
unmounted: 1
commands: 3
```

Of the eight nodes, five are kept, one is updated, one is mounted, and one is unmounted; the commands are three. The screen, the header, and the list stand with the same type, the same place, and equal props. The `value` prop of the counter text goes from `"3 orders"` to `"4 orders"`, so `/Screen/Text` is updated. The two rows with keys `a` and `b` match by their keys and do not change. The row with key `c` has no counterpart in the previous tree, so it is mounted. The `Spinner` in the fourth position has no counterpart in the next tree, so it is unmounted. The commands are the sum of updated, mounted, and unmounted; five of the eight nodes never touch the native side.

The path in the output is built from the root to the node with type names: `/Screen/List/Row[c]` is the row with key `c` inside the list inside the screen. The type names (`Screen`, `Text`, `Row`) are the model's labels; they are not the framework's own component names. When there are several siblings of the same type, what tells the paths apart is the key.

The key's job shows here. Because the rows match by their keys, adding row `c` did not affect rows `a` and `b`. If they had no keys, they would be matched by position; that case will be measured separately in the list performance lesson of the performance and production topic.

#### A Re-render Is Not a Command

Not every lesson builds the tree by hand. In a real framework, code produces the tree from state, so the measurements from here on use a function that produces a tree from state. The function lives in the helper file below; it prints nothing.

```javascript
// view.mjs — produces a view tree from the order state
import { el } from "./fate.mjs";

export function renderOrders(state) {
  const rows = state.orders.map((order) => el("Row", { name: order.name }, [], order.id));
  const children = [
    el("Header", { title: "Orders" }),
    el("Text", { value: `${state.orders.length} orders` }),
    el("List", {}, rows),
  ];
  if (state.loading) children.push(el("Spinner", { visible: true }));
  return el("Screen", {}, children);
}
```

Now we produce the tree twice from the same state. The state did not change; the two trees are two separate objects, but their contents are the same.

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

const state = {
  orders: [
    { id: "a", name: "Coffee" },
    { id: "b", name: "Tea" },
  ],
  loading: false,
};

const before = renderOrders(state);
const after = renderOrders(state);
countFates(reconcile(before, after));
```

```text
kept: 6
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

The tree was regenerated, six of six nodes came out kept, and the commands were zero. A re-render is a piece of work, not a fate. A screen being re-rendered often does not mean commands go often to the native side; what tells this is not the number of renders but the difference between the trees. In the course's later lessons, the number of re-renders and the number of commands will be counted separately.

**CS1.** The model sets up the framework's comparison step only as a node tree. In a real framework, a node's props can also carry function values, style objects, and fields specific to the native view; the model reduces these to values that can be compared with `JSON.stringify`, such as text and numbers. The model builds this fate's rule; the model is not the framework itself. How many views a command creates on the native side, how long it takes, and how many frames it holds are not in the model.

#### State Unmounts or Mounts a Node

The same function describes a node leaving the tree and entering it. When loading finishes, the loading indicator drops out of the description; when loading starts again, it comes back. The measurement below runs the two directions one after the other and each time prints only the records that produce a command.

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

const orders = [
  { id: "a", name: "Coffee" },
  { id: "b", name: "Tea" },
];

const loadingTree = renderOrders({ orders, loading: true });
const doneTree = renderOrders({ orders, loading: false });

console.log("loading finished");
const finished = reconcile(loadingTree, doneTree);
printOps(finished.filter((op) => op.fate !== "kept"));
countFates(finished);

console.log("loading started");
const started = reconcile(doneTree, loadingTree);
printOps(started.filter((op) => op.fate !== "kept"));
countFates(started);
```

```text
loading finished
unmounted: /Screen/Spinner
kept: 6
updated: 0
mounted: 0
unmounted: 1
commands: 1
loading started
mounted: /Screen/Spinner
kept: 6
updated: 0
mounted: 1
unmounted: 0
commands: 1
```

When the indicator left the tree, a single node was unmounted; when it came back, the same node was mounted. In both directions the command count is one and six nodes stayed kept. The fate depends on the direction of the update: the same node is unmounted in one direction and mounted in the other. Mounting happens from scratch. Because the state of an unmounted node disappears, the indicator that comes back is not a continuation of the earlier one but a new one.

There is a distinction here. Removing the indicator from the tree and setting the `visible` prop to `false` are not the same thing. In the second, the node stays in the tree, its single prop changes, and the node is updated; its native view lives on. Which one is right depends on the screen, but the kind of command and the fate of the node's state arise from this choice.

#### The Bounding Measurement

The rule sees the tree's structure, not the native side's work. The rule sees only what is in the tree: when a value that does not enter the tree as a prop changes, the node comes out kept. This is not a contradiction; it is the measure itself.

We increase the order counter in a local variable that does not enter the tree. The variable is a temporary value inside a function; it is not written to the state object and `renderOrders` does not read it. The counter increases, the tree is regenerated. The node in question is `/Screen/Text`.

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

const state = {
  orders: [
    { id: "a", name: "Coffee" },
    { id: "b", name: "Tea" },
  ],
  loading: false,
};

let received = 0;
const before = renderOrders(state);
received += 1;
const after = renderOrders(state);

const ops = reconcile(before, after);
printOps(ops.filter((op) => op.path === "/Screen/Text"));
countFates(ops);
console.log(`received: ${received}`);
```

```text
kept: /Screen/Text
kept: 6
updated: 0
mounted: 0
unmounted: 0
commands: 0
received: 1
```

The counter went up by one and `received: 1` was printed, but `/Screen/Text` came out kept and the commands stayed at zero. The user sees the old count on the screen: because the changed value did not enter the tree as a prop, the framework does not look at it. Since the framework is unaware of the change, it sends no command to the native side; the problem is not in the rule but in the value never being written to the tree. The fix is to write the counter into the state object: then the `value` prop of the `Text` node changes and the node is updated, as in the first measurement.

This limit has a second face. The native side can keep its own state: the cursor position of an input field, the scroll offset of a scrolling list. These values are not in the tree and the rule does not see them. What the rule looks at is only the props in the description.

#### Summary

- This lesson's fate: kept. In the bounding measurement, the counter of the `/Screen/Text` node increased in a variable that does not enter the tree; the node came out kept and the commands stayed at zero.
- In a bridge-based framework, shared code does not draw the screen but describes a view tree; the framework compares the new tree with the previous one and sends commands to the native side only for the nodes that changed.
- The four fates are the same throughout the course: kept, updated, mounted, unmounted. In the first measurement, of eight nodes five came out kept, one updated, one mounted, one unmounted; the commands were three.
- The command count is the sum of updated, mounted, and unmounted. Regenerating the tree is not a command: two trees from the same state gave six kept and zero commands.
- The rule sees only what is in the tree; when a value that does not enter the tree changes, the node stays kept.

#### Next Step

In this lesson the tree was ready and was only compared. The next lesson, `Environment Setup`, asks where the tree comes from in the first place: when an app first opens there is no previous tree, and the whole tree is mounted.

### Environment Setup

Source: https://academia.sh/en/courses/react-native/environment-setup

Environment setup prepares the toolchain needed for shared code to run on two platforms for the first time. The lesson measures that on first launch there is no previous tree and the whole tree is mounted.

In the previous lesson the tree was ready: we had two trees in hand, and the framework's work was only the difference between them. In a real app the first tree also comes from somewhere, and before it there is no tree at all. This lesson covers environment setup: the tools that must be prepared so that shared code can run on two platforms for the first time. Then it measures what that first run does to the view tree.

Setup is a job, not a fate. This lesson's "mounted" has nothing to do with installing tools on a computer; it is the creation from scratch of the native view of a node that appears in the tree for the first time. One word names both meanings; to keep them apart, the course's fate always speaks about a node.

#### The Toolchain

In a bridge-based framework, two separate worlds work side by side. Shared code is written in JavaScript and runs inside a JavaScript engine. The native side is built in each platform's own language with its own toolchain. Environment setup prepares these two worlds together. In general, four parts are needed.

The first is the JavaScript runtime that will run the shared code and a package manager that downloads and manages dependencies. The second is the bundler that gathers the shared code into a single bundle and serves that bundle during development. The third is the native build tools, separately for iOS and for Android; they build the native side's code and turn the app into a runnable package. The fourth is the place where the app will run: an emulator or a physical device.

It is not necessary to learn all of these parts at once; the choice of workflow decides which of them belongs to the team. The distinction is a division of labor for the team: the JavaScript side stays the same on both platforms, while the native side's tools differ by platform. When setup finishes correctly, even the smallest app opens on two platforms and shows a single text on the screen. That text comes from shared code; that is, the bridge has been set up and shared code has sent its first commands to the native side.

#### Two Workflows

**In the managed workflow**, the native project directories are not in the team's hands. The toolchain manages the native side; the team writes only the shared code and a configuration file. When a native build is needed, a managed build service can take on that job. The gain of this workflow is that setup starts fast: a team can begin trying the shared code without managing the tools of two platforms on its own computer. Its cost comes from the same place: when native code must be touched, for example when a capability of the platform that has not yet entered the managed workflow is wanted, the structure in the team's hands may not be enough.

**In the native-project workflow**, the iOS and Android project directories sit in the team's own repository. The team can write native code directly, add native dependencies, and change build settings. The gain is control. The cost is that the toolchains of two platforms fall to the team: two separate build tools, two separate emulators, two separate signing setups.

The choice is mostly the answer to one question: is the team expected to touch native code? If the app will use only ready-made views and shared code, the managed workflow keeps the work simple. If the app depends on a capability of the platform that is not yet ready, or a native module is to be written, the native-project workflow is needed. This decision can change later; moving from a managed project to a native-project one is a job in its own right and is planned.

Between the two workflows there is no difference from the tree's point of view. In both, shared code produces the same description and the framework makes the same comparison. The difference lies before the tree, in how the native side is built.

The configuration file is the first visible face of this choice. The snippet below shows the shape of a configuration of the kind that could exist in a managed workflow. The snippet is not measured; the field names are examples.

```jsonc
// Platform code: configuration
{
  "name": "orders",
  "entry": "App",
  "platforms": ["ios", "android"],
  "orientation": "portrait"
}
```

The file states the app's name, the entry point of the shared code, and the target platforms. The shared code itself is not in this file; the file describes the environment in which the code will run. The configuration values do not enter the tree as props either. This is another form of the limit in the first lesson: when a setting such as the screen orientation changes, no node's prop in the tree changes and the rule produces no fate; the setting's effect is on the native side.

#### The Development Loop

During development, the shared code is served from a development server. The server runs the bundler; when the app starts, it connects to the server and pulls the current bundle. When a file in the shared code changes, the bundle is prepared again and delivered to the running app. This loop works the same way in an emulator and on a physical device.

Because each platform has its own emulator and its own physical device, testing requires two separate lines. The same shared code runs on two platforms, but each platform turns the description into its own native views; so trying a screen on only one platform does not prove that it looks the same on the other.

An emulator is a copy of a platform running inside the computer. It needs no device, and it is easy to try different screen sizes. A physical device, on the other hand, offers behaviors tied to hardware, such as the camera, sensors, notifications, and touch, as they really are. The two environments do not replace each other: a screen that works in the emulator is not assumed to behave the same on a physical device.

One consequence of the loop is important: a change in the shared code mostly does not require rebuilding the native side. A change that touches the native side, for example adding a native dependency, requires a rebuild in the native-project workflow. Knowing which change belongs to which loop is the real everyday benefit of setup. What a reload does to the tree is measured in the course's last lesson.

What is measured here is not setup itself but the first update that appears at the end of setup. When the app first opens, the framework has no previous tree in hand.

#### When There Is No Previous Tree

On first launch the rule asks its first question: if there is no previous node, the node and its whole subtree are mounted. Because there is no previous tree, there is nothing to compare; every node is created from scratch together with its native view. The measurement is made with the comparison core of the first lesson; the core is given again in every lesson in the same form.

```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}`);
}
```

This lesson's helper file is a function that produces a view tree from the setup stage.

```javascript
// app.mjs — produces a view tree from the app state
import { el } from "./fate.mjs";

export function renderApp(state) {
  const children = [el("Shell", { theme: state.theme ?? "light" })];
  if (state.ready) {
    children.push(
      el("Home", {}, [
        el("Header", { title: "Orders" }),
        el("Text", { value: `${state.count} orders` }),
      ]),
    );
  }
  return el("App", {}, children);
}
```

`Shell` is the app's empty shell: a node that carries the color rule and shows nothing else. `Home` is the home screen and enters the tree only when the app is ready. To measure the first launch, we give `null` as the previous tree.

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

const first = renderApp({ ready: true, count: 3 });

const ops = reconcile(null, first);
printOps(ops);
countFates(ops);
```

```text
mounted: /App
mounted: /App/Shell
mounted: /App/Home
mounted: /App/Home/Header
mounted: /App/Home/Text
kept: 0
updated: 0
mounted: 5
unmounted: 0
commands: 5
```

Of five nodes, five came out mounted; kept and updated are zero, and the commands are five. Because no node in the first tree has a counterpart, the command count equals the node count. This is the cost of a first launch: five views are created from scratch on the native side. The model does not tell the size of the cost; the native side's work is outside the rule.

**CS2.** The model sets up the first launch as `reconcile(null, tree)`. In a real app, before the first launch the native side also prepares a window and a root view; these are not in the shared code's tree, so they are not in the model either. The model builds this fate's rule; the model is not the framework itself.

#### The First Tree and the Next Update

Real apps mostly do not show the home screen in the first tree right away. First an empty shell stands; resources are loaded, then the home screen arrives. If we measure these two steps separately, the effect of the first update on the tree changes. This time the previous tree is the empty shell; the next tree also carries the home screen.

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

const shell = renderApp({ ready: false, count: 0 });
const home = renderApp({ ready: true, count: 3 });

const ops = reconcile(shell, home);
printOps(ops);
countFates(ops);
```

```text
kept: /App
kept: /App/Shell
mounted: /App/Home
mounted: /App/Home/Header
mounted: /App/Home/Text
kept: 2
updated: 0
mounted: 3
unmounted: 0
commands: 3
```

In the second update the shell stayed kept: `/App` and `/App/Shell` stood in the same place, with the same type and equal props. The home screen, however, had no counterpart in the previous tree, so it became mounted; its two children also came out mounted along with it, and the mounted node count became three. The command count, five in the first measurement, dropped to three here, because the shell's two nodes had already been mounted on first launch.

#### Where the Problem Begins

Setup is also useful in debugging. If an app does not open, the first question is in which layer the problem is. Three layers are distinguished.

The first is the toolchain: dependencies have not been downloaded, one of the native build tools is missing, or the emulator has not started at all. In this case the app does not run, so the shared code never runs and no tree is ever produced. The question of fate cannot be asked in this layer, because there is no tree to compare.

The second is the bridge: the native side has opened but cannot reach the shared code. During development, a common form of this is the app being unable to connect to the development server. Here too no tree is produced; usually the native side's own error view appears on the screen.

The third is the shared code: the bridge is up and the code has run, but the tree it produced differs from what was expected. Only in this layer does the course's question make sense. There is a tree, an update is being applied to the nodes in the tree, and the update writes a fate to each node.

This distinction gives an order. First the toolchain, then the bridge, and last the tree is checked. Reversing the order, for example looking for a node in the tree while the emulator is not opening, is looking for something that never existed.

#### Two Fates in the Same Update

An update does not have to do the same thing to every node of the tree. While the home screen arrives, the shell's color may also change. In that case the shell, since its type and place are the same, stays where it is but its prop changes; the home screen, being new, becomes mounted. `renderApp` writes the `theme` value in the state to the shell as a prop; the default value is `light`.

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

const shell = renderApp({ ready: false, count: 0 });
const home = renderApp({ ready: true, count: 3, theme: "dark" });

const ops = reconcile(shell, home);
printOps(ops);
countFates(ops);
```

```text
kept: /App
updated: /App/Shell
mounted: /App/Home
mounted: /App/Home/Header
mounted: /App/Home/Text
kept: 1
updated: 1
mounted: 3
unmounted: 0
commands: 4
```

Each of the five nodes received its own fate: the root kept, the shell updated, the home screen and its children mounted. The commands are four. This measurement shows that the rule works node by node. An update does not have a single label; the label is written to each node separately, so the lesson's fate is always the fate of a focus node.

**CS3.** The model counts the managed and native-project workflows as the same from the tree's point of view. In a real workflow, the native side's startup, that is, the preparation of the window and the root view, may change by workflow; this difference is not visible in the tree and the model does not measure it.

#### The Bounding Measurement

This lesson's focus node is `/App/Home`. The bounding measurement puts two updates side by side: the update from the shell to the home screen, and the one that follows it, where the counter goes from three to four. For the first, only the focus node's line is printed; for the second, the home screen's whole subtree; the count covers the second update.

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

const shell = renderApp({ ready: false, count: 0 });
const three = renderApp({ ready: true, count: 3 });
const four = renderApp({ ready: true, count: 4 });

const mount = reconcile(shell, three);
const next = reconcile(three, four);
printOps(mount.filter((op) => op.path === "/App/Home"));
printOps(next.filter((op) => op.path.startsWith("/App/Home")));
countFates(next);
```

```text
mounted: /App/Home
kept: /App/Home
kept: /App/Home/Header
updated: /App/Home/Text
kept: 4
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

The first line is the lesson's fate: in the update where the home screen entered the tree, `/App/Home` came out **mounted**, because it had no counterpart in the previous tree. The limit is in the lines that follow: when the counter changed, the same `/App/Home` stayed **kept** and only `/App/Home/Text` was **updated**. A node's fate depends on the update, not on the node itself. Mounting happens once in a node's life; the rest of its life passes in kept and updated updates.

This distinction is necessary so that setup is not searched for in the wrong place. A problem in the toolchain, for example the emulator not opening, affects no node in the tree: because the tree has not been produced yet. What the rule looks at is not the state of the tools but the two trees.

#### Summary

- This lesson's fate: mounted. The `/App/Home` home screen, in the first update after the shell, was mounted with its whole subtree because it had no counterpart in the previous tree, and the first line of the bounding measurement prints this; in the next update the same node stayed kept.
- Environment setup needs a JavaScript runtime, a bundler, the native build tools of two platforms, and a place to run (an emulator or a physical device); the managed workflow leaves the native side to the toolchain, the native-project workflow leaves it to the team.
- On first launch there is no previous tree: of five nodes, five were mounted and the commands were five.
- In the update from the empty shell to the home screen, two nodes came out kept and three mounted; the commands were three.
- Setup is a job, mounted is a fate; the state of the toolchain is not visible in the tree.

#### Next Step

In this lesson the home screen consisted of a single container, and the node types stayed as labels. The next lesson, `Core Components`, covers core types such as view, text, image, and input field, and measures what a type change does to the tree when an image comes in place of a placeholder text.

### Core Components

Source: https://academia.sh/en/courses/react-native/core-components

The core components are view, text, image, and input field; each is a node in the view tree. The lesson measures what a type change does to the tree in the update where a placeholder text is replaced by an image.

The previous lesson asked where the first tree comes from and found the home screen mounted with its whole subtree. This lesson looks inside that home screen. A screen is built from a few core types the framework offers ready-made: a container view, text, an image, and a field where text is entered. Each is a node in the tree, and the node's type is the third question of the fate rule: if a node of a different type arrives in the same place, the old one is unmounted and the new one is mounted.

The snippet below is the description of a profile card. The snippet is not measured; the model does not run the framework's code.

```jsx
// Platform code: Profile
function Profile({ name, photo }) {
  return (
    <View>
      {photo ? <Image source={photo} /> : <Text>Loading</Text>}
      <Text>{name}</Text>
      <TextInput value={name} />
    </View>
  );
}
```

`View` is a container and shows nothing on its own; it holds its children together. `Text` shows text. `Image` is drawn from an image source. `TextInput` is the field where the user enters text. The function returns an `Image` if the image is ready and a placeholder `Text` if not; the name arrives as a `Text` in both cases. These four types are the core components covered in this lesson; later lessons add new ones with touch and platform selection.

#### Four Types, One Rule

Each of the core types corresponds to a view on the native side. When shared code describes a `Text` node, the platform's own text view is created on iOS and on Android. The native view depends on the node's type; if the type changes, the native view must change too. A text view cannot turn into an image view in place. That is why the rule's third question looks at the type, and if the type differs it does not look at the props: the old subtree is unmounted and the new subtree is mounted.

The core is needed first for the measurement. The comparison core is given again in every lesson in the same form.

```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}`);
}
```

This lesson's helper file produces a view tree from the profile state. The card is a container; its first child is an `Image` if the image is ready and a placeholder `Text` if not. The key of the text that carries the name is `name`, so its place is not affected by changes among its siblings. The input field carries the biography. The second function is an alternative description that makes the placeholder an image too; it is used below for comparison. The image's accessibility label carries the name; for this reason the name can appear in two places on the card.

```javascript
// profile.mjs — produces a view tree from the profile state
import { el } from "./fate.mjs";

export function renderProfile(state) {
  const slot = state.photo === null
    ? el("Text", { value: "Loading" })
    : el("Image", { source: state.photo, label: state.name });
  return el("Card", {}, [
    slot,
    el("Text", { value: state.name }, [], "name"),
    el("Input", { value: state.bio }),
  ]);
}

export function renderProfileImage(state) {
  return el("Card", {}, [
    el("Image", { source: state.photo, label: state.name }),
    el("Text", { value: state.name }, [], "name"),
    el("Input", { value: state.bio }),
  ]);
}
```

#### Same Type, Changed Prop

In the first measurement the user changes the name. The image has not loaded yet; the placeholder stands. Because the type and key stay the same on every node, the rule looks at the props.

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

const before = renderProfile({ name: "Ayse", photo: null, bio: "Loves coffee" });
const after = renderProfile({ name: "Ayse Yilmaz", photo: null, bio: "Loves coffee" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Card
kept: /Card/Text
updated: /Card/Text[name]
kept: /Card/Input
kept: 3
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

Of four nodes, three came out kept and one updated; the commands are one. `/Card/Text[name]` stood in the same place, with the same type and the same key, but its `value` prop changed, so it became updated. The placeholder `Text` and the input field did not change, and no command went to the native side.

#### Where the Data Arrives

We make the same name change while the image is loaded. The name now reaches two places: the text node and the image's label.

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

const before = renderProfile({ name: "Ayse", photo: "profile.png", bio: "Loves coffee" });
const after = renderProfile({ name: "Ayse Yilmaz", photo: "profile.png", bio: "Loves coffee" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Card
updated: /Card/Image
updated: /Card/Text[name]
kept: /Card/Input
kept: 2
updated: 2
mounted: 0
unmounted: 0
commands: 2
```

In the first measurement the same change updated one node, here two. The data is the same, a single state field. Its path through the tree is also the same: from the state held at the root down to the card's children. What changes is what happens where the data arrives. When there was a placeholder, the name reached only the text node; when there is an image, it reaches the image's label too.

This is separate from the question we asked in M31/K03, "in which direction is this data going in the UI tree". There, the data's direction in the tree was classified: down or up. This course classifies not the data's path but what happens at the node it reaches. Two pieces of data with the same direction may update a node or may not affect any; two pieces of data with different directions may also give the same fate.

**CS4.** The model counts the input as controlled: the input field's value is a prop in the tree. A real input field also keeps the typed text on the native side; if the text and the value in the tree drift apart, the rule does not see it. This is an application of the limit from the first lesson: what is not visible in the tree receives no fate.

#### When the Type Changes

Now the image loads and takes the placeholder's place. The card's first child stands in the same place, but its type goes from `Text` to `Image`. The rule stops at the third question: the type is different, the props are not examined.

#### Reading the Snippet Line by Line

We return once more to the profile card snippet given at the start. The `Profile` function takes two inputs: `name` and `photo`. The root of the tree it returns is `View`; its children are, in order, an image or a placeholder, a text, and an input field. The ternary operator selects `Image` if `photo` is set and `Text` if not. The function contains a condition, but the tree does not carry the condition: only the result of the selection is in the tree. The difference between one description and the next is read according to which branch of this condition was selected. The `value` prop given to `TextInput` is the input's content; what the user typed and what stands in the tree need not be the same, and this is stated in the assumption above.

Here is a misconception to clear up: "When the image loads, the placeholder and the image are the same node, only its content changes." In the reader's eye the two stand in the same corner of the card and represent the same thing. In the rule's eye, a `Text` and an `Image` are two separate things: on the native side one is a text view and the other an image view, and it is not possible to turn one into the other. Even if the node's position in the tree is the same, its identity comes with its type.

#### If the Placeholder Were an Image Too

For a counterexample, we change the description: the placeholder becomes an `Image` too, only with an empty source. When the image arrives, the type stays the same and only the source changes.

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

const before = renderProfileImage({ name: "Ayse", photo: null, bio: "Loves coffee" });
const after = renderProfileImage({ name: "Ayse", photo: "profile.png", bio: "Loves coffee" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Card
updated: /Card/Image
kept: /Card/Text[name]
kept: /Card/Input
kept: 3
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

The same change for the same user, with this description, is a single command. The placeholder's native view was not unmounted; the same image view received the new source. The rule does not know the intent inside the description, it looks only at the type and the prop; if the person writing the description wants to see which type changes can be avoided, the measurement tells them. It cannot be said that every placeholder can be an image: turning a loading text into an image changes the meaning. But knowing that a type change has a cost makes it possible to make a choice while writing the description.

#### The Bounding Measurement

The focus node is the old child `/Card/Text`. When the image loads, the `photo` value in the card's state goes from `null` to a source, and the first child becomes `Image` in place of the placeholder `Text`.

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

const before = renderProfile({ name: "Ayse", photo: null, bio: "Loves coffee" });
const after = renderProfile({ name: "Ayse", photo: "profile.png", bio: "Loves coffee" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Card
unmounted: /Card/Text
mounted: /Card/Image
kept: /Card/Text[name]
kept: /Card/Input
kept: 3
updated: 0
mounted: 1
unmounted: 1
commands: 2
```

`/Card/Text` came out unmounted and `/Card/Image` mounted; the commands are two. The two nodes are in the same position, but because their types differ, the rule did not count them as two states of the same node. No comparison was made between the placeholder's props and the image's props. The name text met its own counterpart thanks to its key and stayed kept; the type change next to it did not touch it. The input field stayed kept too.

The cost of an unmount is not only the command count. The native view of the placeholder `Text` and every piece of state it holds disappear; the new `Image` inherits nothing. The placeholder had no state, so nothing was lost here. If the same change had swapped an input field with a text view, the cursor position and selection in the field would have disappeared. This loss is not visible in the tree and is not measured in the model; it is stated in prose.

Now, for comparison, we do the opposite: the type stays the same and only the image's source changes.

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

const before = renderProfile({ name: "Ayse", photo: "profile.png", bio: "Loves coffee" });
const after = renderProfile({ name: "Ayse", photo: "profile2.png", bio: "Loves coffee" });

const ops = reconcile(before, after);
printOps(ops.filter((op) => op.fate !== "kept"));
countFates(ops);
```

```text
updated: /Card/Image
kept: 3
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

`/Card/Image`, standing in the same place, with the same type and the same key, came out updated; the commands are one. The image's native view stayed in place and received the new source. The difference between a type change and a prop change is visible in the command count: the first is two commands, the second one command, and the native view's identity lives on only in the second. If we do not want to pay for one of the two commands, we can make the placeholder an `Image` too. If the placeholder is an `Image`, when the image arrives the type stays the same and only the source prop changes. This is a design choice, and how it looks in the tree is read from these measurements.

**CS5.** The model makes the type comparison over the name. A real framework counts two different description functions as different and uses function identity; the model reduces this subtlety to the type name. The model builds this fate's rule; the model is not the framework itself.

The common lesson of these measurements is this: what determines a node's life is not how it looks on the screen but with which type and which key it is written in the description. Two different looks can be different props of the same type; two similar looks can be different types. When choosing the type, the person writing the description is really choosing this: will this node live as a text view or as an image view? Keeping what will stay the same throughout its life in a single type makes updates cheap; keeping what will live differently in separate types means knowingly accepting unmounting and mounting.

#### Summary

- This lesson's fate: unmounted. In the bounding measurement, the placeholder `/Card/Text` came out unmounted without its props being examined because its type changed when the image arrived; `/Card/Image` was mounted in its place.
- The core components covered in this lesson are view, text, image, and input field; each is a typed node in the tree and corresponds to a view of its type on the native side.
- A prop change on the same type leaves the node updated: when the name changed, `/Card/Text[name]` was updated and the commands stayed at one.
- The same data change gives a different fate depending on the node it reaches: the name updated one node with the placeholder and two with the image.
- The type change produced two commands (one unmounted, one mounted); when only the image's source changed, one command went and the node was updated.

#### Next Step

In this lesson the props were plain values such as text and a source. The next lesson, `The Style System`, covers the style that determines a node's look: style also enters the tree as a prop, and the lesson measures what a new style object whose content stays the same does in the tree.

### The Style System

Source: https://academia.sh/en/courses/react-native/style-system

A style is a prop that determines how a node looks, and it is compared in the tree like any other prop. The lesson measures that a new style object with the same content leaves the node kept, and that a single changed field leaves it updated.

At the end of the previous lesson we said that a node's props are plain values: a text, an image source. A style is a prop too, but its value is not a plain value, it is an object: it carries fields such as width, color, and margin together. The question is this: how does the comparison rule read an object? Are two separate objects that carry the same fields the same or different? The answer is hidden in the rule's fourth question, where it looks at the props, and this lesson measures it.

In the framework, styles are usually defined with a helper and given as a prop of the node. The snippet below is not measured; it only shows where a style stands on the screen.

```jsx
// Platform code: styles
const styles = StyleSheet.create({
  row: { flexDirection: "row", padding: 8 },
  box: { width: 40, height: 40, backgroundColor: "teal" },
});

function Strip() {
  return (
    <View style={styles.row}>
      <View style={styles.box} />
      <Text>Label</Text>
    </View>
  );
}
```

We read the snippet line by line. `StyleSheet.create` takes an object with field names and gathers the styles in one place, named; `row` and `box` are style names. `flexDirection: "row"` determines the main axis of the flexbox layout: the children are lined up side by side. Flexbox layout is the layout rule that places a container's children along an axis; instead of writing each child's position one by one, we tell the container the direction. `padding: 8` leaves space inside the container. The `box` style gives the box its size and color. Two places inside the function matter: `style={styles.row}` binds the container's style, and `style={styles.box}` binds the box's style, to the node's `style` prop. No style is given to the `Text` node; the framework's default applies.

#### A Style Is a Prop

The consequence of this snippet is important. A style is not a separate language or a separate channel; it is an object among the node's props. When a style changes, what happens in the tree is no different from another prop changing. If the node's type is the same and its key is the same, the rule looks at the props; if all of them, the style included, are the same, the node is kept, and if one is different, it is updated. The core is needed first for the measurement.

```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}`);
}
```

This lesson's helper file produces a strip tree from direction, width, and color state; the second function gives the same style to two boxes at once. The strip is a `Row` node; it carries the direction as a prop. Its first child is a `Box`, the second is the label text. The box's style is created as a new object on every call, just as it is on every run of a render function.

```javascript
// strip.mjs — produces a strip tree from direction, width and color
import { el } from "./fate.mjs";

export function renderStrip(state) {
  return el("Row", { direction: state.direction ?? "row" }, [
    el("Box", { style: { width: state.width, color: state.color } }),
    el("Text", { value: state.label }),
  ]);
}

export function renderPair(state) {
  const style = { width: state.width, color: state.color };
  return el("Row", { direction: "row" }, [
    el("Box", { style }),
    el("Box", { style }),
  ]);
}
```

#### Several Fields at Once

An update can change several fields of a style object. In the first measurement both the width and the color of the box change; the sibling text and the container stay the same.

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

const before = renderStrip({ width: 40, color: "teal", label: "Label" });
const after = renderStrip({ width: 80, color: "orange", label: "Label" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Row
updated: /Row/Box
kept: /Row/Text
kept: 2
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

Two fields changed but the command is one. The fate is per node: how many fields changed in a node does not increase how many commands go. In a real app the update command carries which fields changed and the native side applies them one by one; that work is not measured in this lesson. What is measured here is the node coming out updated.

#### The Container's Style

A common misconception is to think that when a style changes, everything beneath it changes too. When the container's direction turns from row to column, the children go to other places on the screen; so in the tree too it looks as if all of them have changed. We measure it.

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

const before = renderStrip({ direction: "row", width: 40, color: "teal", label: "Label" });
const after = renderStrip({ direction: "column", width: 40, color: "teal", label: "Label" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
updated: /Row
kept: /Row/Box
kept: /Row/Text
kept: 2
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

Only `/Row` came out updated; because the children's own props did not change, both are kept. Their moving on the screen is not a fate in the tree. Recalculating the layout is the native side's work and is a consequence of the container being updated. The tree says whose prop changed; it does not say whose position slid on the screen. This is the limit of the style's measure in this course.

Here a distinction from M31/K01 is needed. There, whether two values were equal was the language's own question: equality was an operation that asked whether one value was the same as another, and the answer to the question was a result in its own right. Here equality is only the rule's input. If the rule finds the contents of two style objects equal, it does something with it: it leaves the node kept. What is measured is not the answer to equality but the fate that comes out of it.

#### A Shared Style, Two Nodes

Defining a style once and using it in many places is the real reason for this work. Instead of writing a margin or a color separately in every node, we use a single style that has a name. In the tree, however, this sharing is not visible: each node carries its own prop. When the style changes, every node that uses that style finds a difference within itself.

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

const before = renderPair({ width: 40, color: "teal" });
const after = renderPair({ width: 40, color: "orange" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Row
updated: /Row/Box
updated: /Row/Box
kept: 1
updated: 2
mounted: 0
unmounted: 0
commands: 2
```

A single style changed, but two nodes came out updated and the commands became two. A style being shared does not share the command: the rule looks at nodes, not at style names. This shows that gathering styles in one place gives ease of maintenance but does not reduce the update cost. If we want to keep two nodes together, we need to bind them to a single container and change the container's style; in the earlier sections, the direction change had ended with exactly such a single command.

It is useful to think of the two style approaches side by side. In one, we write a shared prop to every node, and when we change it, all of them are updated. In the other, we put the prop on the container and leave the children to the layout rule. The answer to which is right depends on whether the look is truly shared or only coincidentally the same. The tree does not make this distinction; the count only shows the result of the choice. The decision still belongs to the person writing it.

Here is one more misconception: the thought that "I never changed the style, so the node is never updated". Even if the node's style stays the same, if its text, its image source, or its pressed state changes, the node comes out updated. The rule looks at all the props together; a style is only one of these props. The reverse is also true: a node whose only change is its style, with its content staying the same, is also updated.

A style that comes from state is bound by the same rule. When we want a button to look a different color when it is selected, we select the style with a condition: if the state is selected we give one style, otherwise the other. The tree again sees only the result. Which condition the selection came from, where the condition is written, or how many lines it takes does not concern the rule; what concerns it is whether the style object the node produced differs in content from the previous object. If the condition selects the same style every time, the node stays kept; if it selects a different style, it comes out updated. Defining a style and selecting a style are separate jobs, and the tree carries only the result of the selection.

#### The Bounding Measurement

The focus node is `/Row/Box`, and the measurement has two steps. The first produces the tree a second time with nothing changed. Each call creates a new style object for the box: two separate objects in memory, but with exactly the same fields.

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

const state = { width: 40, color: "teal", label: "Label" };
const ops = reconcile(renderStrip(state), renderStrip(state));
printOps(ops);
countFates(ops);
```

```text
kept: /Row
kept: /Row/Box
kept: /Row/Text
kept: 3
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

All three nodes are kept, and the commands are zero. The model compares props field by field, looking at their contents; two objects being separate is not counted as a difference. Even though `/Row/Box` received a new style object, no command went to the native side. In the second step only the box's color changes; the width, the sibling text, and the root's state stay the same.

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

const before = renderStrip({ width: 40, color: "teal", label: "Label" });
const after = renderStrip({ width: 40, color: "orange", label: "Label" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Row
updated: /Row/Box
kept: /Row/Text
kept: 2
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

A single field of the style object changed and `/Row/Box` came out updated; this is the lesson's first real prop change. The two steps are read together: with the same content, zero commands; with one field different, one command. What makes the difference is the content, not the object being recreated. That is why this lesson's fate is kept: a new object having arrived changes nothing; updated comes out only when a field is truly different.

**CS6.** The model compares style objects by content. A real framework may compare props by object identity; in that case, a style object recreated on every run could be counted as a difference every time. In this model there is no place that compares by object identity, and this lesson does not measure that behavior. What is measured is clear: what the rule does while the content is the same. One reason why defining a style once and reusing it is recommended in a real framework is this; but the number for that reasoning is not given here.

**CS7.** Real styles can also be given as an array, and the framework merges them into a single object. The model counts a style as a single plain object. The merging of arrays and the dropping of invalid fields are not visible in the tree.

A style change did not unmount and mount any node. As long as the type and key stay the same, however much the look changes, a node comes out at most updated. The way to unmount is to change the type or the key; the type change was measured in the previous lesson. The model builds this fate's rule; the model is not the framework itself.

In short, the style system is not a mechanism outside the tree. Styles may stand in a separate file in terms of where they are defined, in a named table; when they enter the tree, they become only a prop value. The comparison does not look at where the style is defined, it looks at the value the node carries. For this reason every question about style comes down to the same three questions: is the type the same, is the key the same, is the content of the props the same? If the answer to all three is yes, the node stays kept, and where and how the style is written does not change the result.

One last note on style: changing a single field of a style and replacing the whole style object with another object can give the same result in the tree. The number of changed fields does not concern the rule; what concerns it is whether the object is equal in content to the previous object.

#### Summary

- This lesson's fate: kept. In the bounding measurement, a new style object with the same content left `/Row/Box` kept and the commands were zero; then, when a single field changed, the same node came out updated, with the lesson's first real prop change.
- A style is an object that stands among the node's props; it is compared in the tree like any other prop.
- The model compares objects by content: if the content is the same, a new object is not counted as a difference. Comparison by object identity is not in this model and is not measured.
- When one or several fields change, the node comes out updated and a single command goes; siblings and children stay kept.
- When the container's direction changes, only the container is updated; the children sliding on the screen is not a fate in the tree.

#### Next Step

Until now we changed the tree ourselves: we gave the state by hand with two separate values. The next lesson, `Touch and Interaction`, covers the event that changes the tree: a touch changes a prop, and the lesson measures which nodes this change reaches.

### Touch and Interaction

Source: https://academia.sh/en/courses/react-native/touch-and-interaction

A touch changes state, state changes a prop, and the prop leaves a node updated. The lesson measures that the pressed state updates only the button and that its siblings stay kept.

Until now we put two trees side by side ourselves: the previous one and the next one. On a real screen, what produces the next tree is an event, most often a touch. The finger meets the screen, the framework passes the event to the handler of the relevant node, the handler changes the state, and the screen is described again from the new state. At the end of this chain the rule asks the same question again: which node is kept and which is updated?

The snippet below shows a button that responds to touch. The snippet is not measured.

```jsx
// Platform code: Action
function Action({ label, onPress }) {
  const [pressed, setPressed] = useState(false);
  return (
    <Pressable
      onPressIn={() => setPressed(true)}
      onPressOut={() => setPressed(false)}
      onPress={onPress}
    >
      <Text>{pressed ? "Pressed" : label}</Text>
    </Pressable>
  );
}
```

`Pressable` is a container that responds to touch. `onPressIn` runs when the finger lands, `onPressOut` when it lifts, and `onPress` when a press is completed. The handlers change a state value, and this value determines the content of the node. A touch is not a node in the tree; only the trace it leaves is visible in the tree.

#### From Event to Node

The chain has four links: event, state, prop, node. The event is outside the tree. The state is kept outside the tree and causes the screen to be described again. The prop is the value the description gives to a node. The node is what the rule assigns a fate to. The model has no place for the event; there are only two consecutive descriptions. This has a benefit: however the event arrived, the fate is read from the difference between the two descriptions.

```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}`);
}
```

This lesson's helper file produces a button tree from the pressed state. The screen consists of a title, a button, and a note line. The button carries the `pressed` prop; this prop is the face of the pressed state in the tree. The content of the title and the note is not affected by the event.

```javascript
// button.mjs — produces a screen tree from the pressed state
import { el } from "./fate.mjs";

export function renderButton(state) {
  return el("Screen", {}, [
    el("Text", { value: "Order" }),
    el("Button", { label: state.label, pressed: state.pressed }),
    el("Text", { value: state.note }),
  ]);
}
```

#### When the Finger Lifts

When the finger lifts, `onPressOut` runs and `pressed` becomes false again. The tree returns to its first form.

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

const pressedTree = renderButton({ label: "Confirm", pressed: true, note: "Two items" });
const releasedTree = renderButton({ label: "Confirm", pressed: false, note: "Two items" });

const ops = reconcile(pressedTree, releasedTree);
printOps(ops.filter((op) => op.fate !== "kept"));
countFates(ops);
```

```text
updated: /Screen/Button
kept: 3
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

Pressing and releasing are symmetric: each is a single updated node and a single command. In this model a whole touch is two commands, and both go to the same node. The button was never unmounted and mounted; the same native view was updated twice.

#### The Changed Node, Not the Touched Node

A common misconception is to assume that the touched node will come out updated. The finger touched the button, so the button changed. But the rule looks not at the touch but at the difference between descriptions. Touching a button may change a text somewhere else on the screen, and the button itself may change none of its props. We measure it: the touch updates only the note line, and the button stays the same.

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

const before = renderButton({ label: "Confirm", pressed: false, note: "Two items" });
const after = renderButton({ label: "Confirm", pressed: false, note: "Three items" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Screen
kept: /Screen/Text
kept: /Screen/Button
updated: /Screen/Text
kept: 3
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

The button was the target of the touch and came out kept; the text node that carries the note came out updated. The touch target, that is, the area the finger touches, is not a prop of the tree; only the props of nodes stand in the tree. There is no link between the target and the fate; the link in between is built by the state the handler changes. For this reason, before predicting the number of commands, the question "which state changed" is asked, not "what was touched".

We read one more snippet line by line. The `Action` function given at the start holds a `pressed` value with `useState`. `onPressIn` makes the value true and `onPressOut` makes it false; `onPress` runs the handler that comes from outside and fires once when the press is completed. The `Text` node shows a fixed text if pressed and its label if not. The function returns a single tree; the pressed form and the normal form are two separate trees, and the difference is only in the content of that text. The same job can also be done by giving the button a `pressed` prop; which form is preferred in the tree does not change the rule, it only changes which node the difference appears in.

#### One Touch, Many States

Now the touch changes not only the pressed state but also the note: when the press is completed, the handler changes the label and writes the updated order count to the note line.

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

const before = renderButton({ label: "Confirm", pressed: true, note: "Two items" });
const after = renderButton({ label: "Confirmed", pressed: false, note: "Three items" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Screen
kept: /Screen/Text
updated: /Screen/Button
updated: /Screen/Text
kept: 2
updated: 2
mounted: 0
unmounted: 0
commands: 2
```

A single event changed three fields: the button's label, the button's pressed prop, and the content of the note. Two nodes came out updated in the tree; the commands are two. The button's two fields were counted as one command; the fate is per node. The title stayed kept again. How many nodes a touch reaches depends on which states the handler changes; the touch itself does not determine a number.

A distinction needs to be made here. What determines the number is not the event but the state difference the event leaves. The same touch can cause zero commands on one screen and five commands on another. The number of commands is not predicted by looking at the touch; the two descriptions have to be compared.

#### If the Same State Arrives Again

Touch events arrive frequently. While the finger slides slightly on the screen, the framework may pass the same event several times in a row, and the handler rewrites the state each time. Writing the state and changing the state are not the same thing: when the handler makes the `pressed` value true and the value is already true, no difference arises in the tree. We measure it.

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

const first = renderButton({ label: "Confirm", pressed: true, note: "Two items" });
const again = renderButton({ label: "Confirm", pressed: true, note: "Two items" });

const ops = reconcile(first, again);
printOps(ops);
countFates(ops);
```

```text
kept: /Screen
kept: /Screen/Text
kept: /Screen/Button
kept: /Screen/Text
kept: 4
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

All four nodes are kept, and the commands are zero. Writing the same value again produces a new description, but because the description is equal to the previous one, the rule sends no command. This is the counterpart on the touch side of what the first lesson taught, that a re-render and a command are separate things: however often the events arrive, it is the difference that determines the command. Whether a heavy stream of events puts a load on the tree depends not on the number of events but on how different the state left by the events is.

#### A Press Is Three Events

To understand a press as a whole, the events have to be separated. `onPressIn` runs when the finger lands, `onPressOut` when it lifts. If the finger slides outside the target between the two, the press is canceled and `onPress` never runs. If the finger lifts inside the target, the press counts as completed and `onPress` runs. So the pressed feedback is always undone, but the action is taken only on a completed press. Two separate things must not be confused: the feedback is a prop change in the tree, while the action is the real work that changes the application's state.

This distinction is reflected in the result in the tree as well. A canceled press amounts to the pressed form being opened and closed in the tree: two updates, no lasting difference. A completed press, in addition to these two updates, carries the difference that comes with the action changing the state; many of the states we measured above were of this kind. Telling the two presses apart by looking only at the tree is possible if something changed in the end; if nothing changed, both leave the same two commands.

The size of the touch target is also a design matter. A small target makes it hard to hit with a finger, and the user may touch the wrong place. The framework allows defining extra space to enlarge the target; this space enlarges the area the finger can touch without enlarging the look. This value may appear in the tree as a prop, but it was not taken into the model in this lesson. The reason: the rule looks only at whether a prop has changed, and the target size changing on its own is a rarely seen situation. What matters is to remember that nothing not written in the tree receives a fate; enlarging the target is the native side's job, and it either never enters the tree or enters it as a prop.

#### The Bounding Measurement

The focus node is `/Screen/Button`, and the measured moment is the `onPressIn` moment. The state changes only one field: `pressed` goes from false to true.

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

const before = renderButton({ label: "Confirm", pressed: false, note: "Two items" });
const after = renderButton({ label: "Confirm", pressed: true, note: "Two items" });

const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Screen
kept: /Screen/Text
updated: /Screen/Button
kept: /Screen/Text
kept: 3
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

Of four nodes, three are kept and one is updated; the commands are one. The touch makes the whole screen be described again starting from the root, but the rule finds a difference only in `/Screen/Button`. The title, the note, and the container are described again, compared in the tree, and come out kept. This is why describing again is cheap: the expensive thing is the native command, and it goes only where there is a difference.

It is also possible to keep the same feedback only on the native side: the pressed look changes inside the native view itself and never returns to the shared code. In that case nothing changes in the tree and the button stays kept. This is not measured, because there is no difference to measure: the rule sees only what is in the tree. Writing the pressed state into the tree as a prop is a choice, not a necessity; it was written in this lesson so that the trace the touch leaves in the tree could be shown.

**CS8.** The model carries `pressed` as an ordinary prop. In the real framework the pressed state is often handled on the native side and changes the look without ever returning to the shared code. In that case no command is visible in the tree either. The model keeps this path closed and brings the pressed state into the tree deliberately; what is measured is what happens if the state enters the tree.

**CS9.** The model does not run the event's handler. It gives the state the handler produces directly to the describing function. In the real framework, state updates can be batched and several touches can merge into a single new description; the model counts each state as a separate description.

The model builds this fate's rule; the model is not the framework itself.

The practical rule that comes out of this lesson's measurements is short: to see the trace a touch leaves in the tree, one must first ask which state changed, and then which node reads that state. If the answers to the two questions point to the same node, that node is updated. If they point to different nodes, the touched node stays kept. If they point to no node, that is, if the function that produces the tree does not read the changed state, the tree comes out the same and the commands are zero; the counter measurement in the first lesson was exactly this case.

Finally, we note that in all the measurements the touch itself has no fate. A fate belongs only to nodes. As long as a touch does not turn into a trace in the tree, that is, as long as it does not change the value of a prop, it counts as not having happened for the rule.

#### Summary

- This lesson's fate: updated. In the bounding measurement, because the pressed state was written to the tree as a prop, `/Screen/Button` came out updated and its siblings and the container stayed kept; if the same feedback had been kept only on the native side, the node would have stayed kept.
- The chain is event, state, prop, and node; in the tree only the prop and the node are visible.
- When the pressed state is carried as a prop, a touch leaves a single node updated and a single command goes.
- Pressing and releasing are separate updates, one command each; the button is never unmounted and mounted.
- What determines the number of commands is not the touch but the state difference the touch leaves.

#### Next Step

Until now, two descriptions belonged to the same condition of the same framework. In the last lesson, `Platform-Specific Code`, we cover the case where the same screen is described differently on two platforms, and we measure what the difference leaves in the tree.

### Platform-Specific Code

Source: https://academia.sh/en/courses/react-native/platform-specific-code

The same screen can be described differently on two platforms. The lesson compares two platform trees, shows that a toggle switch present on only one platform comes out mounted, and states plainly that this is not a runtime update.

In the previous lessons we measured how a single tree changes over time. In this lesson there are two trees, and the two never exist at the same time: one runs on iOS, the other on Android. While describing a screen, shared code can look at the platform and produce different nodes. The question then is this: how similar are the trees of the two platforms, and how does the difference appear in the tree?

One thing must be stated clearly from the start. An app is at any moment on either iOS or Android; at runtime it does not move from one platform to the other. Comparing the two trees, that is, deriving a fate from one platform's tree to the other's, is not a runtime update. Here the comparison is a measuring tool: it shows where the two trees part ways.

Code that asks about the platform looks like this in the framework. The snippet is not measured.

```jsx
// Platform code: Row
function Row({ label }) {
  return (
    <View>
      <Text>{label}</Text>
      {Platform.OS === "ios" ? <Switch /> : <Checkbox />}
    </View>
  );
}
```

`Platform.OS` gives the name of the platform it runs on. The expression is a condition: if it is `ios`, a toggle switch is returned, otherwise a checkbox. The same container and the same label, but the type of the last child differs by platform.

#### Shared Code, Diverging Description

The framework's core types are shared by the two platforms; what is platform-specific is which of those types is selected. Shared code makes this selection inside the describing function, and two separate trees arise. The core is needed first for the measurement.

```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}`);
}
```

This lesson's helper file produces a settings screen tree from the platform name. On both platforms the screen carries a title and a label. On Android there is additionally a toggle switch; on iOS this node does not exist. The second function produces the same row with a different type on the two platforms; it is compared below.

```javascript
// settings.mjs — produces a settings screen tree from the platform name
import { el } from "./fate.mjs";

export function renderSettings(platform) {
  const children = [
    el("Text", { value: "Settings" }),
    el("Text", { value: "Notifications" }),
  ];
  if (platform === "android") {
    children.push(el("Toggle", { on: true }));
  }
  return el("Settings", {}, children);
}

export function renderChoice(platform) {
  return el("Row", {}, [
    el("Text", { value: "Notifications" }),
    platform === "ios" ? el("Switch", { on: true }) : el("Checkbox", { on: true }),
  ]);
}
```

#### How Much Is Shared

The first measurement compares the same platform's tree with itself. This is a reference that shows shared code is consistent within itself: the platform does not change, so the fate does not change either.

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

const ops = reconcile(renderSettings("ios"), renderSettings("ios"));
printOps(ops);
countFates(ops);
```

```text
kept: /Settings
kept: /Settings/Text
kept: /Settings/Text
kept: 3
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

Three nodes are kept, and the commands are zero. This is the reference: if the iOS description is compared with the iOS description, there is no difference. Now we compare the two platforms.

#### The Difference Between the Two Platforms

The second measurement compares the iOS tree with the Android tree. The container and the two texts exist on both; the toggle switch is only on Android.

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

const ops = reconcile(renderSettings("ios"), renderSettings("android"));
printOps(ops);
countFates(ops);
```

```text
kept: /Settings
kept: /Settings/Text
kept: /Settings/Text
mounted: /Settings/Toggle
kept: 3
updated: 0
mounted: 1
unmounted: 0
commands: 1
```

The three common nodes are kept, and `/Settings/Toggle` is mounted. The share of shared code is three nodes, and the platform-specific share is one node. We read this as follows: the screens of the two platforms are equal in three of four nodes. This is a count, not a ratio; the number is valid only for these two trees.

The word mounted must be read carefully here. An app running on iOS never mounts that toggle switch, because it never sees Android's tree. The comparison is not a transition but a distinction: the answer to the question "which extra node is there on Android".

**CS10.** The model produces the trees of the two platforms in the same process and at the same time. In reality only one is produced, and the platform it runs on stays fixed throughout the app. This model is a convenience needed to make a comparison, not runtime behavior.

#### The Platform Module

Alongside file selection, the framework offers a small module that asks which platform it is running on. The module gives the platform's name and contains a helper for selecting values from a table: one value is written for iOS and another for Android, and the running platform's value is returned. These two ways do the same job; in one the selection is made inside an expression, in the other in the file name. For small differences, for example for a margin to be different on the two platforms, the module is enough; if a whole screen fragment needs to be written separately for the two platforms, the file path stays cleaner.

A single question helps when deciding: is the divergence a detail of the look, or the behavior itself? If it is a detail, it appears in the tree as a prop difference and comes out updated in the comparison. If it is behavior, different types or different nodes arise in the tree and the comparison gives mounted and unmounted. A prop difference keeps the shared share high; a type difference lowers it. For this reason, making the divergence in the smallest possible node is the way to protect the shared share.

#### Selection by File Extension

Writing a condition is not the only way to diverge by platform. The framework can choose between files that carry the same name but state the platform in the extension: one file written for iOS and another written for Android stand side by side, and the import writes only the shared name. The running platform decides which file to choose; there is no `if` in the code.

```jsx
// Platform code: Toggle
// Toggle.ios.js    -> file written for iOS
// Toggle.android.js -> file written for Android
import Toggle from "./Toggle";

function Row() {
  return <Toggle on={true} />;
}
```

The first two lines in the snippet are comments showing the file names; the code imports only the shared name. Which file will be used is chosen when the app is bundled: a separate bundle is built for each platform, and the file with the `.ios` extension goes into the iOS bundle and the file with the `.android` extension into the Android bundle. The benefit of this method is that platform-specific code stays in separate files and shared files do not fill up with conditions. Its cost is that the two files can drift apart: if one is updated and the other is forgotten, the two platforms quietly differ. The tree shows this drift when we compare the trees of the two platforms.

#### The Same Place, a Different Type

Platform-specific divergence is not only an extra node. The node in the same position can be of a different type on the two platforms: a toggle switch on iOS, a checkbox on Android. In this case the comparison sees the previous lesson's type change again, this time across platforms.

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

const ops = reconcile(renderChoice("ios"), renderChoice("android"));
printOps(ops);
countFates(ops);
```

```text
kept: /Row
kept: /Row/Text
unmounted: /Row/Switch
mounted: /Row/Checkbox
kept: 2
updated: 0
mounted: 1
unmounted: 1
commands: 2
```

The container and the label came out kept, the toggle switch unmounted, the checkbox mounted; the commands are two. Of four nodes, two are common. In the previous measurement the platform-specific share was a single node; here it is two nodes: the same position, but one type in place of another. The same screen being "the same" on two platforms does not mean the nodes are the same in the tree. A single codebase does not guarantee the same screen; what it guarantees is that the screen is produced from the same source on the two platforms.

Here is a misconception to pick out: "I wrote shared code, so the two platforms show the same screen." Shared code can produce two trees, and if the trees differ, the native side builds different views. The person who wants to raise the shared share can use this comparison as a checking tool: every node that diverges is either a platform difference left in deliberately or a drift that came in without being noticed. The tree does not say which is which; it only shows where they part.

#### The Bounding Measurement

The focus node is `/Settings/Toggle`. The last measurement also tests the reverse of the divergence: when one looks from the Android tree to the iOS tree, the toggle switch disappears.

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

const ops = reconcile(renderSettings("android"), renderSettings("ios"));
printOps(ops);
countFates(ops);
```

```text
kept: /Settings
kept: /Settings/Text
kept: /Settings/Text
unmounted: /Settings/Toggle
kept: 3
updated: 0
mounted: 0
unmounted: 1
commands: 1
```

When the direction changed, the fate changed too: looking from Android to iOS, the toggle switch came out unmounted; looking from iOS to Android, it came out mounted. The three common nodes are kept in both directions. A comparison has a direction, and the fate depends on that direction; but whichever direction one looks from, the size of the common and diverging share is the same.

This lesson's fate is mounted, but its limit must be written from the start: this mounted did not come from an update that happens at runtime. It came from comparing one tree with the other. This is the measure we use when looking at platform-specific code: in how many nodes the two trees are common, and in how many they diverge.

This differs from the question of M32/K01. In that lesson we measured sharing: how much of the code is common to two platforms. Here sharing was not measured; what was measured is what fate a node in the trees of the two platforms receives in the comparison. We are not saying that a node that is common also corresponds to the same native view on the platform.

**CS11.** The model counts the shared types as the same on the two platforms. In reality the same type produces a different native view depending on the platform; the type name is the same in the tree and the difference is not visible.

The model builds this fate's rule; the model is not the framework itself.

As a last measure, we put the two measurements side by side. On the settings screen, three of four nodes were common and one diverged. On the choice row, two of four nodes were common and two diverged. In the first case the divergence adds something to the screen, in the second it changes something. A person who wants to raise a screen's shared share should first look at which kind of divergence it is; because the addition can be solved with a conditional node, and the change by choosing only the type of that node, and neither touches the container or the siblings.

The limit of these measurements must be stated too. The trees we compared carry only types and props; how the native views of the two platforms look on the screen, which typeface they use, or how they compute margins is not in the tree. For this reason the result "the shared share is high" does not mean the screens look alike; it only means the descriptions are alike. For the looks to be alike, one has to look with one's own eyes as well, and this course's measure does not cover that.

Finally, we stress once more why this lesson is not a runtime update, because the word `mounted` in the measurement output can mislead. The app produces the same tree from start to finish on the platform it is installed on. Because the platform does not change, `renderSettings("android")` and `renderSettings("ios")` are never called one after the other in an app's life. The updates that happen at runtime happen within a platform, as the state changes, and the measurements of the earlier lessons showed them. In this lesson, putting the two trees side by side is a tool used to make the divergence visible.

There is a small trap when counting the share of shared code: the number depends on the size of the tree. On a screen of four nodes, one diverging node looks like a large share; on a screen of a hundred nodes, the same single node hardly stands out. For this reason, before comparing numbers across screens, one has to look at the node counts. In this lesson every number is valid only for the two trees of that measurement and is not generalized. A person who wants to generalize sets up the same measurement again on their own screens.

A checklist also helps. Before adding a divergence, these are asked: does the difference really come from the platform, or from indecision in the design? Is the diverging node the smallest node? Was the counterpart of the divergence in the two trees measured and the shared share recorded? If the answers to the three questions are clear, the divergence counts as made deliberately.

#### Summary

- This lesson's fate: mounted. In the second measurement, `/Settings/Toggle`, found only on Android, came out mounted when one looked from the iOS tree to the Android tree; this is not a runtime update but a comparison of two trees.
- A description that diverges by platform produces two separate trees; the app sees only one of them.
- The share of shared code is counted by the common nodes and the platform-specific share by the diverging nodes: in four nodes, three common and one diverging.
- The direction of the comparison determines the fate: the same node comes out mounted in one direction and unmounted in the other.
- The same type name does not guarantee the same native view on the two platforms; the tree does not show this difference.

#### Next Step

The core structure is completed here: tree, components, style, touch, and platform difference. The next topic, `Navigation`, covers what moving between screens does to the tree: when one screen goes and another comes, which nodes are unmounted and mounted.

## Application Layers

Navigation, local and shared state, networking and offline behavior, local and secure storage, notifications and deep linking, gestures and animations.

### Navigation

Source: https://academia.sh/en/courses/react-native/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.

### State Management

Source: https://academia.sh/en/courses/react-native/state-management

State management decides where the data that changes over time on a screen is kept: local to a single component or shared and read by many screens. The lesson measures how one state change updates the cart badge and leaves the product list kept.

Navigation determined when screens are mounted and unmounted in the tree. The data inside a screen changes over time: the number of items in a cart, the text in an input field, whether a section is open or closed. This data is called state, and state management means deciding where the state is kept and who reads it.

The question of the course does not change: **what does this update do to the node in the view tree?** The fate is kept, updated, mounted, or unmounted; the total of the updated, mounted, and unmounted nodes is the commands. We do not assign a fate by hand; the rule gives it:

```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 model builds this fate's rule; the model is not the framework itself. The rule looks at trees; it does not see where the state sits.

#### Local State

Local state is data that a single component keeps inside itself. The quantity counter of each row in a cart is an example: nobody outside the row reads this number. In the framework local state is held with a state hook, and the hook asks for the component to be rendered again when the state changes. A render is the component producing its new view tree; producing a tree does not count as a command on its own.

```jsx
// Platform code: QuantityRow
function QuantityRow({ name }) {
  const [qty, setQty] = useState(1);
  return (
    <Pressable onPress={() => setQty(qty + 1)}>
      <Text>{name}: {qty}</Text>
    </Pressable>
  );
}
```

This fragment is not measured. In the model, state is the input of the function that builds the tree: the function produces a tree from the state, and the rule compares the previous tree with the next one.

**AL1.** The model assumes that the functions from state to tree are pure: the same state gives the same tree. Real components also have hooks and side effects, but what this lesson measures is only the tree.

We build a list of two rows. The quantity of each row is its own state, and the quantity of the `pen` row goes from 1 to 2.

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

function renderRows(counts) {
  return el("List", {}, Object.keys(counts).sort().map((name) =>
    el("Item", { label: name, count: counts[name] }, [], name)));
}

const ops = reconcile(
  renderRows({ pen: 1, notebook: 1 }),
  renderRows({ pen: 2, notebook: 1 }),
);
printOps(ops);
countFates(ops);
```

```text
kept: /List
kept: /List/Item[notebook]
updated: /List/Item[pen]
kept: 2
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

When local state changes, only its owner is updated. `Item[pen]` is **updated**, because its `count` prop changed; `Item[notebook]` and the list are **kept**. The command count is 1. Keeping the state local keeps the effect of the change local too.

#### Shared State and Lifting State Up

More than one row reads the cart count. The badge in the top bar, the total in the bottom bar, and the checkout button all look at the same number. This data can no longer live inside a single component, because sibling components cannot read each other's state. The solution is to move the state to the common ancestor of the components that read it: this operation is called **lifting state up**. The ancestor holds the state and passes it down as a prop.

If there is a deep chain between the ancestor and the components below it, passing the prop through every layer becomes heavy. Then the shared state is put into a **provider**: this component, placed at the top of the tree, lets every component below it read the state directly. Shared state libraries offer this mechanism ready-made; the reading component only calls a hook.

```jsx
// Platform code: CartBadge
function CartBadge() {
  const count = useCartCount();
  return <Text>{count}</Text>;
}
```

**AL2.** The model does not show the provider as a node in the view tree; it shows only the nodes that read the count. The cart count is in shared state, and `renderCart(state)` builds the tree from this state.

```javascript
// cart.mjs — builds the cart screen's view tree from the shared state
import { el } from "./fate.mjs";

export function renderCart(state) {
  return el("Screen", {}, [
    el("Header", {}, [
      el("Title", { text: "Cart" }),
      el("Badge", { count: state.count }),
    ]),
    el("List", {}, state.items.map((label) => el("Item", { label }, [], label))),
  ]);
}
```

The user adds one more of a product in the cart. The cart count goes from 2 to 3, and the list of products does not change.

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

const items = ["pen", "notebook"];
const ops = reconcile(
  renderCart({ count: 2, items }),
  renderCart({ count: 3, items }),
);
printOps(ops);
countFates(ops);
```

```text
kept: /Screen
kept: /Screen/Header
kept: /Screen/Header/Title
updated: /Screen/Header/Badge
kept: /Screen/List
kept: /Screen/List/Item[pen]
kept: /Screen/List/Item[notebook]
kept: 6
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

A single state change was made, and two nodes received different fates. `/Screen/Header/Badge` is **updated**, because the badge's `count` prop changed. The product rows and the list are **kept**, because their props stayed the same. In M30/K03 the question was what triggers a recomputation. This course does not ask the cause; it classifies the effect: not the cause but the effect, and the same trigger updates one node and keeps another.

The price of sharing is also visible in this measurement. As the number of nodes that read a state grows, the same change produces more commands. We compare this in two setups. In one, the badge keeps its own counter, and the total at the bottom shows another number. In the other, both read the same shared state.

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

function renderPair(badge, total) {
  return el("Screen", {}, [
    el("Header", {}, [el("Badge", { count: badge })]),
    el("Footer", {}, [el("Total", { count: total })]),
  ]);
}

console.log("local:");
countFates(reconcile(renderPair(2, 2), renderPair(3, 2)));
console.log("shared:");
countFates(reconcile(renderPair(2, 2), renderPair(3, 3)));
```

```text
local:
kept: 4
updated: 1
mounted: 0
unmounted: 0
commands: 1
shared:
kept: 3
updated: 2
mounted: 0
unmounted: 0
commands: 2
```

In the local setup the change updates a single node: commands 1. In the shared setup two nodes are updated: commands 2. In the local setup two places on the screen show different numbers: badge 3, total 2. In the shared setup both show 3, and the price of this is one command. Which setup is right comes not from the command count but from the screen being consistent. The rule does not make this judgment; it tells the price of a command.

#### Adding an Item to a List

Until now, the state change modified a prop that already existed. Most real changes do not stop at changing a number; they also add an item to a list. When a product is added to the cart, two things happen: the badge count increases and a new row appears in the list. The two effects come from the same state but receive different fates in the tree; we measure this next.

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

const ops = reconcile(
  renderCart({ count: 2, items: ["pen", "notebook"] }),
  renderCart({ count: 3, items: ["pen", "notebook", "eraser"] }),
);
printOps(ops.filter((op) => op.fate !== "kept"));
countFates(ops);
```

```text
updated: /Screen/Header/Badge
mounted: /Screen/List/Item[eraser]
kept: 6
updated: 1
mounted: 1
unmounted: 0
commands: 2
```

The badge is **updated**, as in the previous measurements. The new row, however, is not updated but **mounted**: in the previous tree there was no node with this key, and the rule cannot update a node that does not exist. The rows of the first two products match by their keys and stay **kept**. The key of the rows is the product name; for this reason the rule knows from keyed matching, not from position, that the addition does not touch the old rows.

This measurement also explains a distinction. The state is a single object, but its effect on the tree is in parts: the node that shows the number is updated, the item entering the list is mounted, and the untouched items are kept. To say "one state change is one command" is wrong; the command count depends on how many nodes the change reaches.

#### Counterexample: The Cost of Lifting Up

Lifting state up is not always a good move. If the quantity of a row concerns only that row, moving it to the shared state at the very top of the screen brings no gain. The data is now the state of the whole screen; the change modifies the state at the top, and every node fed by that state enters the comparison. The two-setup measurement above showed this: in the shared setup a single change was two commands, in the local one it was one. As the number of nodes grows, the difference grows.

The price of leaving it local, however, is inconsistency: if two places show the same number, the two showing different numbers is a bug. The decision is therefore not a rule but a weighing: look at how many places read the data and whether those places need to be consistent. If consistency is needed, one extra command of shared state is accepted; if it is not needed, the state stays local.

#### Where State Should Live

The place of state is found with three questions. The first is who reads it: if only one component reads it, it stays local. The second is where the common ancestor of the readers is: if two siblings read it, the state lives in the ancestor right above them. The third is whether the readers are in distant corners of the tree: if so, a provider steps in. Keeping the state higher than necessary looks like a convenience at first glance, but with every change all the components that read that state ask for a re-render. A re-render is not a command; the rule compares trees and counts only the difference as commands. Even so, re-rendering many components needlessly means the rendering work grows for nothing, and this work is measured as a separate topic in later lessons.

#### Common Misconceptions

The first misconception is the idea that "when state changes, the screen changes." The screen changes only if the function from state to tree writes the change into the tree. As the bounding measurement will show, a field that is not written to the screen updates no node.

The second misconception is that "shared state is always better." Shared state makes a piece of data readable from many places, but it brings a price too: the change produces as many commands as there are nodes that read it, and it becomes harder to track who changed the data. Data that concerns only its own component, such as the quantity of a row, should stay local.

The third misconception is to choose between local and shared once and leave it. A piece of data may start out local and be lifted when a new reader is added to the screen. Moving it is a change in the arrangement of the code; the nodes in the view tree stay the same, and what changes is only which node reads which state.

The fourth misconception is to store a value that can be derived separately. The badge count can be computed from the length of the list. Keeping it as a separate state creates the possibility of the two values drifting apart: the list may show three products while the badge says two. A value that can be derived is not stored; it is computed on every render. This way, when an item is added to the list, the badge is also updated by the same change, as seen in the addition measurement, and the two cannot drift apart.

The fifth misconception is to assume that the smaller a state is, the more harmless it is. A small state can also be read by many nodes. The cost is determined not by the size of the state but by the number of nodes that read it.

The sixth misconception is to count the measurement of a single state change as the behavior of the whole app. What is measured here is the effect of one change on one cart screen. On another screen, with more readers or a deeper tree, the numbers change; what does not change is the way the fate is classified.

#### The Bounding Measurement

A state changing and a node being updated are not the same event. Below, a field that is not shown on the screen, `coupon`, is added to the cart state. First only this field changes, then the count changes.

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

const items = ["pen", "notebook"];
const focus = (op) => op.path === "/Screen/Header/Badge";
const first = reconcile(
  renderCart({ count: 2, items, coupon: "summer" }),
  renderCart({ count: 2, items, coupon: "winter" }),
);
const second = reconcile(
  renderCart({ count: 2, items, coupon: "winter" }),
  renderCart({ count: 3, items, coupon: "winter" }),
);
console.log("coupon changed:");
countFates(first);
console.log("count changed:");
printOps(second.filter(focus));
```

```text
coupon changed:
kept: 7
updated: 0
mounted: 0
unmounted: 0
commands: 0
count changed:
updated: /Screen/Header/Badge
```

The coupon changed, but `renderCart` does not write this field to the tree; the two trees are the same and commands is 0. The state changed, the tree did not. When the count changes, `/Screen/Header/Badge` is **updated**. The rule does not see the place of the state or how much it changed; it sees only the comparison of the props visible in the tree. For this reason state management alone is not a performance measure: a state being updated a lot does not mean it produces many commands.

#### Summary

- This lesson's fate: updated. When the cart count changed, the `/Screen/Header/Badge` node came out updated, because its `count` prop changed.
- The same state change updated the badge and left the product list kept: the same trigger gave two nodes two different fates.
- Local state keeps the effect of a change on the node that owns it; shared state produces as many commands as there are nodes that read it.
- When a state that is not written to the tree, such as a coupon, changes, no node is updated: a state change and a command are not the same thing.
- The rule does not see the place of the state; it only compares the previous and the next tree.

#### Next Step

In this lesson the state changed inside the app. The app's most important data source, however, is outside: a server. The next lesson, `Networking and Data`, measures what the loading indicator and the list go through in the tree when a request returns a response.

### Networking and Data

Source: https://academia.sh/en/courses/react-native/networking-and-data

The networking layer carries a request to the screen through loading, error, and data states; an offline cache changes the first state. The lesson measures that when data arrives, the loading indicator is unmounted and the list is mounted.

The previous lesson measured how state is held inside the app. The app's most important data source, however, is outside: a server. Data from the server does not reach the screen at once. The request goes out, the response comes back, and in between the screen has to show the user something. This lesson covers that gap and the cache that closes it.

The question of the course does not change: **what does this update do to the node in the view tree?** The fate is kept, updated, mounted, or unmounted; the total of the updated, mounted, and unmounted nodes is the commands. We do not assign a fate by hand; the rule gives it:

```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 model builds this fate's rule; the model is not the framework itself. The rule compares trees; it does not see what the request does on the network.

#### The Request Layer and Three States

An app does not send network requests from scattered places. The request layer collects the address, the headers, and the error handling in one place; screens only say "give me the data at this address." A request is a job spread over time, and its result returns as a promise (`Promise`): it either resolves with data or is rejected with an error. While the request is in progress, the screen is said to be in one of three states. In the **loading** state the response has not arrived yet. In the **data** state the response has arrived and the list is shown. In the **error** state the request has been rejected and the screen tells the user so.

These three states are three different nodes on the screen: a loading indicator, a list, and an error message. In the framework a request is usually started in a side effect hook: when the screen appears for the first time, the hook runs, the request goes out, the state changes when the response arrives, and the screen is rendered again.

```jsx
// Platform code: OrderFeed
function OrderFeed() {
  const [status, setStatus] = useState({ phase: "loading", rows: [] });
  useEffect(() => {
    loadOrders().then((rows) => setStatus({ phase: "data", rows }));
  }, []);
  return <Feed status={status} />;
}
```

This fragment is not measured; the model does not run the framework or the network. In the model a request is a `Promise` chain in its own file, and the server is a fixed table held in memory.

**AL1.** The model replaces the real network with a table in memory: a known address resolves with data, an unknown address is rejected. Latency and disconnection do not enter the model; the model measures only which state the response arrives with.

```javascript
// request.mjs — the request layer: sends a request to the in-memory fake server with a Promise
const SERVER = {
  "/orders": [
    { id: "a1", text: "Pen" },
    { id: "b2", text: "Notebook" },
  ],
};

export function request(url) {
  return new Promise((resolve, reject) => {
    if (url in SERVER) resolve(SERVER[url]);
    else reject(new Error("not found"));
  });
}
```

`renderFeed(status)` builds the screen's tree from the request state. In the loading state there is an indicator under the title, in the data state a list, in the error state a message. The rows are keyed by id.

```javascript
// feed.mjs — builds the order feed screen's view tree from the request state
import { el } from "./fate.mjs";

export function renderFeed(status) {
  let body;
  if (status.phase === "loading") {
    body = el("Spinner");
  } else if (status.phase === "error") {
    body = el("Notice", { text: status.message });
  } else {
    body = el("List", {}, status.rows.map((row) =>
      el("Row", { text: row.text }, [], row.id)));
  }
  return el("Feed", {}, [el("Title", { text: "Orders" }), body]);
}
```

First the request succeeds. The screen opens in the loading state and turns into the data state when the response arrives.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderFeed } from "./feed.mjs";
import { request } from "./request.mjs";

const loading = renderFeed({ phase: "loading" });
request("/orders")
  .then((rows) => renderFeed({ phase: "data", rows }))
  .catch(() => renderFeed({ phase: "error", message: "could not load" }))
  .then((next) => {
    const ops = reconcile(loading, next);
    printOps(ops);
    countFates(ops);
  });
```

```text
kept: /Feed
kept: /Feed/Title
unmounted: /Feed/Spinner
mounted: /Feed/List
mounted: /Feed/List/Row[a1]
mounted: /Feed/List/Row[b2]
kept: 2
updated: 0
mounted: 3
unmounted: 1
commands: 4
```

When the data arrives, `/Feed/Spinner` is **unmounted**. The list and its two rows are mounted in its place. The title and the screen itself are **kept**. The command count is 4, and three of them are the nodes of the new list. Because the indicator and the list share the same place in the tree, the rule does not count them as two states of the same node: their types differ, one leaves entirely and the other is mounted entirely.

The error state is another outcome of the same transition. We repeat the same flow with an address the server does not know.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderFeed } from "./feed.mjs";
import { request } from "./request.mjs";

const loading = renderFeed({ phase: "loading" });
request("/missing")
  .then((rows) => renderFeed({ phase: "data", rows }))
  .catch(() => renderFeed({ phase: "error", message: "could not load" }))
  .then((next) => {
    const ops = reconcile(loading, next);
    printOps(ops);
    countFates(ops);
  });
```

```text
kept: /Feed
kept: /Feed/Title
unmounted: /Feed/Spinner
mounted: /Feed/Notice
kept: 2
updated: 0
mounted: 1
unmounted: 1
commands: 2
```

The indicator is again **unmounted**, but the only node mounted in its place is the error message: commands 2. Every path out of the loading state unmounts the indicator. Which node will be mounted is decided by the result of the request.

#### The Offline Cache

An offline cache keeps the last successful response in the app's memory or in local storage. If there is no network or the response is late, the screen shows the last known data. This changes the first state of the screen: if there is a cache, the screen opens not with a loading indicator but directly with the list.

```javascript
// cache.mjs — offline cache: keeps the last successful response in memory
const store = new Map();

export function saveCache(url, rows) {
  store.set(url, rows);
}

export function readCache(url) {
  return store.has(url) ? store.get(url) : null;
}
```

We measure side by side the two situations in which the screen is opened for the first time: opening without a cache and opening with a cache. The tree does not exist yet, so the previous tree is `null` and every node is mounted.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderFeed } from "./feed.mjs";
import { saveCache, readCache } from "./cache.mjs";

saveCache("/orders", [
  { id: "a1", text: "Pen" },
  { id: "b2", text: "Notebook" },
]);
console.log("uncached:");
countFates(reconcile(null, renderFeed({ phase: "loading" })));
console.log("cached:");
countFates(reconcile(null, renderFeed({ phase: "data", rows: readCache("/orders") })));
```

```text
uncached:
kept: 0
updated: 0
mounted: 3
unmounted: 0
commands: 3
cached:
kept: 0
updated: 0
mounted: 5
unmounted: 0
commands: 5
```

Opening with a cache mounts more nodes: five instead of three. But the screen never shows an indicator, so there is no indicator to be unmounted later. A cache means more work at launch and less work in the next transition: when the response arrives, the list's nodes are not mounted again; they are compared where they stand.

#### Retrying After an Error

The error state is not the end of the road; the user often says "try again." For the tree, a retry is a return: the error notice goes away and the loading indicator appears again. These two nodes replace each other under the root.

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

const ops = reconcile(
  renderFeed({ phase: "error", message: "could not load" }),
  renderFeed({ phase: "loading" }),
);
printOps(ops.filter((op) => op.fate !== "kept"));
countFates(ops);
```

```text
unmounted: /Feed/Notice
mounted: /Feed/Spinner
kept: 2
updated: 0
mounted: 1
unmounted: 1
commands: 2
```

The root and the title are **kept**, because they have the same type and carry the same props in every state. The error notice is **unmounted** and the indicator is **mounted**; even though the two nodes are in the same position, their types differ, so the rule does not count one as the updated version of the other. This shows that the cost of a retry in the tree is constant: neither the length of the error message nor the number of the attempt changes the command count.

There is a subtle point here. If the retry fails, the screen returns to the error state and the notice is mounted again. With consecutive failed attempts, the user sees the indicator and the notice being mounted and unmounted continually. This is not a tree problem but a design question: keeping the old error notice while the retry is in progress calms the tree, but it does not tell the user that the attempt has ended. The rule says only how many commands each option is; it does not say which is better.

#### The Bounding Measurement

A value that comes from the cache may have become stale by the time the response arrives. In M30/K04 the question is whether a screen agrees with the model, and a stale screen is one outcome. The fate measured in this lesson does not measure agreement: a stale value can also sit in a kept node. If the new response is the same as the old value, the node is kept, whether the value is current or the response never arrives at all. The measurement below compares two situations. In one, the new response carries the same rows as the cache. In the other, the text of the second row has changed.

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

const cached = [
  { id: "a1", text: "Pen" },
  { id: "b2", text: "Notebook" },
];
const stale = renderFeed({ phase: "data", rows: cached });
const same = renderFeed({ phase: "data", rows: cached.map((row) => ({ ...row })) });
const changed = renderFeed({
  phase: "data",
  rows: [cached[0], { id: "b2", text: "Notebook 2" }],
});

console.log("same response:");
countFates(reconcile(stale, same));
console.log("changed response:");
printOps(reconcile(stale, changed).filter((op) => op.fate !== "kept"));
```

```text
same response:
kept: 5
updated: 0
mounted: 0
unmounted: 0
commands: 0
changed response:
updated: /Feed/List/Row[b2]
```

In the same response all nodes are **kept** and commands is 0. In the changed response only `Row[b2]` is **updated**. In the first case the rule says "nothing happened," but this does not mean the cache is current: if the response had never arrived, the same tree would have stayed kept in the same way. Stale and kept are not the same question. The first is whether the value matches the truth on the server, and it lies outside the tree. The second is what the update did to the node.

#### Common Misconceptions

The first misconception is the idea that "the loading indicator is hidden when the list arrives." In terms of the tree, the indicator is not hidden; it leaves the tree: the node is unmounted and its native view is gone. If loading is requested again, the indicator is mounted anew, and no trace of the old one remains.

The second misconception is the idea that "a cache reduces network load, so it makes the screen lighter too." Opening with a cache, as seen in the measurement, mounts more nodes. The gain is the disappearance of the indicator and of the waiting, not a lower command count.

The third misconception is to treat the error state as an exception. An error is one of the ordinary results of the request layer and an ordinary node in the tree. The screen knowing how to mount the error node ensures that no empty screen is left in the error state.

The fourth misconception is to assume that a retry is cheap and invisible in the tree. In the retry measurement the error notice is unmounted and the indicator is mounted: two commands. These two commands repeat with every failed attempt. If the attempts fail one after another, the screen, in the user's eyes, goes back and forth between error and loading. In terms of the tree, every round trip is the same two commands; whether this feels tiring to the user is a question outside the measurement.

The fifth misconception is to treat the request and the screen as one thing. The request returns a promise and reaches one of three results; the screen turns this result into a tree. The request layer produces the error message, and the tree only carries this message as a prop of a node. If the two layers stay separate, the request layer can be tested without touching the screen, and the screen can be rendered without running the request.

The sixth misconception is to treat the offline cache as independent of data freshness. The list in the cache is the last successful response and may be older than the current state on the server. The tree does not see this: if the rows from the cache and the rows from the server have the same form, the rule does not tell the two apart. If we want to show the user the age of the data, we need to write it into the tree explicitly, for example by adding a separate node.

The seventh misconception is to treat the three states as independent screens. Loading, error, and data swap the body under the same root node; the root and the title are kept in all three states. For this reason the cost of a transition between states is measured not by the size of the screen but by the size of the body.

#### Summary

- This lesson's fate: unmounted. When data arrived, the `/Feed/Spinner` node came out unmounted because it gave its place to the list; the list and its rows are mounted.
- In the error state too the indicator is unmounted and an error message is mounted in its place; every path out of the loading state unmounts the indicator.
- Opening with a cache mounts more nodes but mounts no indicator; opening without a cache mounts the indicator and unmounts it later.
- If the new response carries the same value as the cache, the rows are kept; the row that changed in the response is updated.
- The fate does not measure agreement: a stale value can sit in a kept node.

#### Next Step

In this lesson the cache stayed in memory and disappeared when the app closed. Data that must persist needs local storage. The next lesson, `Local Storage`, measures how a draft read from storage at launch appears in the tree.

### Local Storage

Source: https://academia.sh/en/courses/react-native/local-storage

Local storage keeps the data that must remain even when the app closes: a key-value store for small values, an embedded database for records that are queried. The lesson measures that a draft read from storage at launch is mounted in the tree, and that writing to storage does not change the tree.

The previous lesson covered data from the server and the cache in memory. Memory is emptied when the app closes. If a draft the user typed, a setting the user chose, or a downloaded list should still be there when the app is opened again, the data is written to local storage. This lesson covers the two kinds of storage and how data gets from the store into the tree.

The question of the course does not change: **what does this update do to the node in the view tree?** The fate is kept, updated, mounted, or unmounted; the total of the updated, mounted, and unmounted nodes is the commands. We do not assign a fate by hand; the rule gives it:

```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 model builds this fate's rule; the model is not the framework itself. The rule compares trees; it does not see where the data is stored.

#### The Key-Value Store

A key-value store binds a name to a value: the name `draft` to a piece of text, the name `theme` to a choice. Reading and writing are a single step, and whoever knows the name gets the value. It suits small, independent data that does not need to be queried: settings, the last screen that was opened, a half-finished text. Access to the store is asynchronous so that it does not hold up the app's main flow: a read returns a promise.

An asynchronous read means that the value is not yet at hand when the screen is first rendered. The screen opens first without the value, the state changes when the store responds, and the screen is rendered again.

```jsx
// Platform code: DraftEditor
function DraftEditor() {
  const [draft, setDraft] = useState(null);
  useEffect(() => {
    storage.getItem("draft").then(setDraft);
  }, []);
  return <Editor draft={draft} />;
}
```

This fragment is not measured; the model does not run the store or the framework.

**AL1.** The model builds the store as an in-memory `Map` and returns reads and writes with a `Promise`. The app closing and opening again does not enter the model; the model measures only what happens in the tree when the read finishes.

```javascript
// store.mjs — key-value store: reads and writes with a Promise over an in-memory Map
const data = new Map();

export function setItem(key, value) {
  return Promise.resolve().then(() => {
    data.set(key, value);
  });
}

export function getItem(key) {
  return Promise.resolve().then(() => (data.has(key) ? data.get(key) : null));
}
```

**AL2.** The model counts the draft as not yet read when the screen is first rendered; the tree is built first without the draft. If there is a draft, the `Draft` node enters the tree under the screen; if there is not, it never enters. `renderEditor(state)` builds the tree from this state.

```javascript
// editor.mjs — builds the draft screen's view tree from the state
import { el } from "./fate.mjs";

export function renderEditor(state) {
  const children = [el("Title", { text: "Draft" })];
  if (state.draft !== null) children.push(el("Draft", { text: state.draft }));
  return el("Editor", {}, children);
}
```

The app opens. There is a draft saved earlier in the store. The screen is first built without the draft, and when the read finishes, the draft state enters the tree.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderEditor } from "./editor.mjs";
import { setItem, getItem } from "./store.mjs";

const opening = renderEditor({ draft: null });
setItem("draft", "Hello")
  .then(() => getItem("draft"))
  .then((saved) => {
    const ops = reconcile(opening, renderEditor({ draft: saved }));
    printOps(ops);
    countFates(ops);
  });
```

```text
kept: /Editor
kept: /Editor/Title
mounted: /Editor/Draft
kept: 2
updated: 0
mounted: 1
unmounted: 0
commands: 1
```

`/Editor/Draft` is **mounted**. The title and the screen itself are **kept**. The command count is 1, and that command is the data read from the store. The store is outside the tree: data enters the tree only through state, and the tree knows it only as a node.

If the store is empty, the state does not change. The read returns `null`, and the tree is the same as the tree at launch.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderEditor } from "./editor.mjs";
import { getItem } from "./store.mjs";

const opening = renderEditor({ draft: null });
getItem("missing").then((saved) => {
  console.log("empty store:");
  countFates(reconcile(opening, renderEditor({ draft: saved })));
});
```

```text
empty store:
kept: 2
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

When the store is empty, the read produces no command. A read having finished is not enough for the tree to change; what changes the tree is the value that was read changing the state.

#### The Embedded Database

If the data is not a single value but many records, and the records will be queried, a key-value store is not enough. An embedded database runs inside the app and keeps records in tables. Records are looked up by their id, filtered by a condition, and sorted. A list of drafts, pinned drafts, and records sorted by date are examples.

**AL3.** The model builds the database as a fixed table of rows with ids and limits the query to a single filter: `query(onlyPinned)` returns all the rows or only the pinned ones.

```javascript
// db.mjs — embedded database: queries in-memory rows with a Promise
const ROWS = [
  { id: "t1", title: "Intro", pinned: true },
  { id: "t2", title: "Result", pinned: false },
  { id: "t3", title: "Appendix", pinned: true },
];

export function query(onlyPinned) {
  return Promise.resolve(ROWS.filter((row) => !onlyPinned || row.pinned));
}
```

The query result enters the screen as a list. The rows are keyed by id; this way, when a filter changes, which rows remain is known from the id.

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

function renderDrafts(rows) {
  return el("List", {}, rows.map((row) =>
    el("Item", { title: row.title }, [], row.id)));
}

Promise.all([query(false), query(true)]).then(([all, pinned]) => {
  const ops = reconcile(renderDrafts(all), renderDrafts(pinned));
  printOps(ops);
  countFates(ops);
});
```

```text
kept: /List
kept: /List/Item[t1]
kept: /List/Item[t3]
unmounted: /List/Item[t2]
kept: 3
updated: 0
mounted: 0
unmounted: 1
commands: 1
```

The filter removed the row that is not pinned. `Item[t2]` is **unmounted**, and the remaining two rows and the list are **kept**. Because the rows are keyed by id, the `t3` row was not counted as a different row even though its position shifted. This is a result independent of the kind of store: the embedded database gives the tree only rows, and what the rule sees is the row.

#### Reading the Source Fragment Line by Line

The `DraftEditor` fragment shows in three parts how the store connects to the tree. The first line starts the state with `null`: when the screen is first rendered, the draft does not exist yet. This is the framework counterpart of the model's first assumption above. The second part is the side effect that runs once at launch: when the promise read from the store finishes, the result is written as state. The third part is the render: the screen shows whatever the state is.

In the fragment, the only path from the store to the tree is state. The store does not add a node directly to the tree; the read finishes, the state changes, and the screen is rebuilt from that state. The model's `renderEditor(state)` function corresponds to this last step. The only difference between them is this: the model puts the end of the read in order with a promise chain, while the framework catches the same end with a side effect. The fate seen in the tree is the same on both paths, because the rule compares trees and does not look at how they were reached.

One consequence of this reading is the following: in the first render the user sees, for a brief moment, a screen without the draft. If the store is fast, this moment goes unnoticed; if it is slow, the screen opens empty first and the draft appears later. This has a counterpart in the tree: the `Draft` node is mounted not in the first build but in the second comparison, and our measurement compares exactly these two steps.

#### Deleting the Draft

The opposite of reading and writing is deleting. When the user throws the draft away, the state becomes `null` again and the `Draft` node must leave the tree. This is measured too.

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

const ops = reconcile(
  renderEditor({ draft: "Hello" }),
  renderEditor({ draft: null }),
);
printOps(ops.filter((op) => op.fate !== "kept"));
countFates(ops);
```

```text
unmounted: /Editor/Draft
kept: 2
updated: 0
mounted: 0
unmounted: 1
commands: 1
```

The reverse of launch happened: the node that was mounted became unmounted. The title and the screen are **kept**. The deletion produced one command; but this command is the tree's command, and deleting from the store is a separate job. Removing the draft from the tree does not delete the value in the store. When the app is opened again, the old draft in the store is read again and `Draft` is mounted again. For the draft the user threw away not to come back, both the state and the store must be cleared; the tree sees only the first.

#### Which Store for What

The two kinds of store give the same fate in the tree but are chosen for different jobs. A key-value store carries a single value whose name is known, and its read and write steps are simple. It is enough for small data that needs no query. An embedded database gives the data a structure: rows, ids, filters. If the data itself is a collection and the user filters and sorts it, a database is the right choice.

The choice can go wrong in two directions. Serializing a collection into a single key and rewriting it from the beginning on every change causes every small change to read and write all of the data. Keeping a single setting value in a database builds an unnecessary structure. The tree does not see this difference, but the way the screen reacts is affected by this choice: because a database gives rows with ids, the list tree can be keyed, while getting keys out of a single serialized value takes extra work.

#### Common Misconceptions

- "When something is written to the store, the screen changes." Writing changes no node in the tree. What changes the screen is the state changing; a save leaves a copy of the state outside the tree.
- "Reading from the store fills the screen at once." A read is asynchronous. The screen is first built without the value and is updated by a second comparison when the read finishes; this gap can be seen, even if only for a moment.
- "An empty store is an error." An empty store ends the read with `null` and the state does not change. The tree stays the same as the tree at launch, and commands is 0.
- "Data that has left the tree has left the store too." An unmounted node does not delete the value in the store. The data the user threw away does not come back at the next launch only if it is deleted from the store as well.
- "A database is always better than a key-value store." Structure is a price. Building a table for a single setting and thinking about row ids brings unnecessary work; the choice looks at the shape of the data, not at the power of the store.
- "The native view of an unmounted node stays in the store." A native view such as a scroll position goes away with the tree; what stays in the store is only the data the app explicitly wrote. If what the user should see on returning was not written, it is lost.
- "The store needs to be read at every launch." A read is needed only for the data the screen needs. Reading at launch a value the screen never shows does needless work; the tree does not see this work, but the app's launch process carries it.
- "If saving takes time, the screen should wait." Writing is asynchronous and the tree does not wait for the write to finish. The user sees the text they typed instantly; the completion of the save is tracked separately.
- "The data in the store and the data on the screen are always the same." The two sit in different places and may differ until the write finishes. While the user types, the screen is ahead and the store is behind; the tree shows only the state of the screen.

#### The Bounding Measurement

Writing to the store does not change the tree. When the user edits the draft, the state changes and `Draft` is updated in the tree. Writing the draft to the store is a separate job, and when the write finishes nothing new appears in the tree. The measurement below separates the two steps: first the user types, then the text is saved to the store.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderEditor } from "./editor.mjs";
import { setItem } from "./store.mjs";

const opened = renderEditor({ draft: "Hello" });
const typed = renderEditor({ draft: "Hello world" });
console.log("typing:");
printOps(reconcile(opened, typed).filter((op) => op.path === "/Editor/Draft"));
setItem("draft", "Hello world").then(() => {
  console.log("save:");
  countFates(reconcile(typed, renderEditor({ draft: "Hello world" })));
});
```

```text
typing:
updated: /Editor/Draft
save:
kept: 3
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

Typing **updated** the `Draft` node, because the text prop changed. Saving to the store, however, changed no node: commands is 0 and the `Draft` node is **kept**. The store is a write outside the tree; the rule does not see it and does not need to. Even if the save failed, the tree would stay the same. For this reason, whether a save was done correctly is not understood by looking at the tree; it is understood by reading back from the store.

#### Summary

- This lesson's fate: mounted. When the draft read from the store at launch entered the tree, the `/Editor/Draft` node came out mounted.
- When the store is empty, the read produces no command: the tree does not change unless the read changes the state.
- In an embedded database the query result is a list; when the filter changes, the row that leaves is unmounted and the remaining rows are kept because they are matched by id.
- Writing to storage produces no command in the view tree; editing updates the `Draft` node, while saving changes no node.
- The rule does not see the kind of the store or the success of the write; it only compares trees.

#### Next Step

In this lesson the data written to the store was ordinary text such as a draft. Some values are not ordinary: a session token, a password, a key. These should not sit in a plain store. The next lesson, `Secure Storage`, measures where secret values are kept and what happens in the tree when the session closes.

### Secure Storage

Source: https://academia.sh/en/courses/react-native/secure-storage

Secret values such as a session token, a password, and a key do not sit in a plain store; they are written to the operating system's protected store. The lesson measures that when the session closes, the account subtree is unmounted, while deleting the token from the store does not show in the tree.

The previous lesson covered data that remains even when the app closes. That the data was ordinary text such as a draft made the job easier. A session token, a password, or an encryption key is not ordinary: whoever obtains them takes the user's place. This lesson covers where secret values are kept and what happens in the view tree when the session closes.

The question of the course does not change: **what does this update do to the node in the view tree?** The fate is kept, updated, mounted, or unmounted; the total of the updated, mounted, and unmounted nodes is the commands. We do not assign a fate by hand; the rule gives it:

```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 model builds this fate's rule; the model is not the framework itself. The rule compares only trees; it does not see where a secret value is kept.

#### Where a Secret Value Lives

A plain key-value store sits in the app's data directory, in an unprotected file. It can end up in a backup and can be read from a rooted phone. For a secret value, the operating system offers a separate place: a secure key store. The value is kept there encrypted, and access is tied to the app's identity and often to the lock state of the phone. Each platform's store comes with its own name and its own rules; the framework puts a single write and read surface on top of them.

```swift
// Platform code: SecretStore
import Security

func saveToken(_ token: Data) -> Bool {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "session-token",
        kSecValueData as String: token,
        kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
    ]
    SecItemDelete(query as CFDictionary)
    return SecItemAdd(query as CFDictionary, nil) == errSecSuccess
}
```

This fragment is not measured; the model does not run the platform's store. The point to notice is that writing is a call entirely separate from the app's view: the code that writes to the store draws nothing on the screen.

**AL1.** The model builds the secure store as an in-memory `Map` and returns reading, writing, and deleting with a `Promise`. It does not model encryption or the access conditions; what is measured is what writing to the store or deleting from it leaves in the tree.

```javascript
// vault.mjs — secure key store: in-memory Map, write, read, and delete with a Promise
const secrets = new Map();

export function saveSecret(key, value) {
  return Promise.resolve().then(() => {
    secrets.set(key, value);
  });
}

export function readSecret(key) {
  return Promise.resolve().then(() => (secrets.has(key) ? secrets.get(key) : null));
}

export function deleteSecret(key) {
  return Promise.resolve().then(() => {
    secrets.delete(key);
  });
}
```

**AL2.** The model builds the app's tree from the session state: `Login` if there is no token, `Account` with `Profile` and `Orders` under it if there is. The title is there in both states. The value of the token itself does not enter the tree; the tree looks only at whether a token exists.

```javascript
// app.mjs — builds the app's view tree from the session state
import { el } from "./fate.mjs";

export function renderApp(session) {
  const body = session.token === null
    ? el("Login", {})
    : el("Account", {}, [el("Profile", { name: session.name }), el("Orders", {})]);
  return el("App", {}, [el("Header", {}, [el("Title", { text: "My Account" })]), body]);
}
```

In the framework this choice is a small screen gate that looks at the token state.

```jsx
// Platform code: AccountGate
function AccountGate({ token }) {
  return token ? <Account /> : <Login />;
}
```

#### When the Session Closes

The user closes the session. The token in the state is deleted and the gate returns to the `Login` screen.

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

const signedIn = renderApp({ token: "t-1", name: "Alice" });
const signedOut = renderApp({ token: null, name: "" });
const ops = reconcile(signedIn, signedOut);
printOps(ops);
countFates(ops);
```

```text
kept: /App
kept: /App/Header
kept: /App/Header/Title
unmounted: /App/Account
unmounted: /App/Account/Profile
unmounted: /App/Account/Orders
mounted: /App/Login
kept: 3
updated: 0
mounted: 1
unmounted: 3
commands: 4
```

`/App/Account` is **unmounted**, and with it the `Profile` and `Orders` under it are unmounted too: the subtree goes as a whole. `Login` is mounted in its place. The title and the app itself are **kept**. The screen state belonging to the user's account, the scroll position, the field the user half typed, all disappear with these nodes. This result agrees with the distinction in M10/K02: the state of what is unmounted disappears; the fate classifies not the place of the lifetime but what the update does to the node.

#### Reading the Token at Launch

When the app is opened again, the state in memory is empty. Only the secure store knows whether the session continues. At launch the token is read from the store and the state is built accordingly.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderApp } from "./app.mjs";
import { saveSecret, readSecret } from "./vault.mjs";

const opening = renderApp({ token: null, name: "" });
saveSecret("session", "t-1")
  .then(() => readSecret("session"))
  .then((token) => {
    const ops = reconcile(opening, renderApp({ token, name: "Alice" }));
    printOps(ops);
    countFates(ops);
  });
```

```text
kept: /App
kept: /App/Header
kept: /App/Header/Title
unmounted: /App/Login
mounted: /App/Account
mounted: /App/Account/Profile
mounted: /App/Account/Orders
kept: 3
updated: 0
mounted: 3
unmounted: 1
commands: 4
```

The launch screen starts with `Login`. When the token is found, `Login` is unmounted, and `Account` and the two nodes under it are mounted. What is seen in the tree is not the read from the store itself but the read changing the state. If the token had not been found, the state would not change and the tree would stay the same as the tree at launch.

#### Deleting Does Not Show in the Tree

Closing a session is two separate jobs: clearing the state and deleting the token from the store. The first changes the tree. The second happens outside the tree. The measurement below does only the second: the token is deleted from the store while the state stays in place.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderApp } from "./app.mjs";
import { saveSecret, readSecret, deleteSecret } from "./vault.mjs";

const session = { token: "t-1", name: "Alice" };
saveSecret("session", "t-1")
  .then(() => deleteSecret("session"))
  .then(() => readSecret("session"))
  .then((left) => {
    console.log("left in store: " + left);
    countFates(reconcile(renderApp(session), renderApp(session)));
  });
```

```text
left in store: null
kept: 6
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

The store was emptied but nothing happened in the tree: commands is 0, and `Account` still stands where it was. The screen looks at the token in the state; it does not look at what is left in the store. For this reason deleting alone is not enough, and alone it does not show.

#### Reading the Platform Fragment Line by Line

The `saveToken` function carries four decisions together. The first is the class of the query: the value is defined as a generic password record. The second is the name of the record: the session token is found under a single name, so the code that reads and the code that deletes use the same name. The third is the value itself. The fourth is the access condition: the value can be read only while the phone is unlocked and cannot be moved to another phone.

Before writing, a delete call is made with the same query. The reason is that these stores may refuse to add a second record under the same name; deleting first prevents the write from failing when the token is renewed. The function finally returns whether the add succeeded.

No line of this fragment touches the screen. Whether the write succeeds or fails, the view tree is the same. The only thing that reaches the screen is the app writing this result as state: the token was received, the session is open. This distance between the store and the tree is the common subject of this lesson's measurements.

#### When the Token Is Renewed

Tokens have a limited lifetime; from time to time the app gets a new token and replaces the value in the store. The user should not notice this. For the tree too, it should not be noticed: the session continues, the screen stays the same. We measure this with two renewals. In the first only the token changes. In the second the user's name changes along with the token.

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

const before = renderApp({ token: "t-1", name: "Alice" });
console.log("token only:");
countFates(reconcile(before, renderApp({ token: "t-2", name: "Alice" })));
console.log("token and name:");
printOps(
  reconcile(before, renderApp({ token: "t-2", name: "Zoe" }))
    .filter((op) => op.fate !== "kept"),
);
```

```text
token only:
kept: 6
updated: 0
mounted: 0
unmounted: 0
commands: 0
token and name:
updated: /App/Account/Profile
```

In the first renewal the tree did not change: because the token is not written to the tree, commands is 0. The user sees nothing in terms of the tree, and the account screen stays where it was. In the second renewal only `Profile` is **updated**, because the prop that changed is the prop of the node that shows the name. `Account` and the other nodes under it are **kept**.

These two results are two faces of the same principle. A secret value does not enter the tree; for this reason its change leaves no trace in the tree. The information that enters the tree is the information visible on the screen, and a node is updated only when that changes. A secret value not entering the tree is not only a leak precaution; it also prevents needless commands.

#### Counterexample: Writing the Token into the Tree

It may occur to someone to make the token a screen prop for convenience: the token is carried as a prop of the `Account` node. This works, but it has two prices. The first is that with every renewal `Account` becomes updated; a command arises although nothing on the screen changed. The second, and the heavier one, is that the secret value is now a part of the view tree. The tree travels in error reports, in debugging output, and in screen recordings; the secret value travels with it. The store exists to protect the value, and copying it into the tree breaks this protection.

#### The Bounding Measurement

The fate does not measure the security of a secret value. The same session closing is done in two ways: with the token left in the store and with the token deleted from the store. The tree changes the same way in both cases.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderApp } from "./app.mjs";
import { saveSecret, readSecret, deleteSecret } from "./vault.mjs";

function signOut(withDelete) {
  const signedIn = renderApp({ token: "t-1", name: "Alice" });
  const signedOut = renderApp({ token: null, name: "" });
  const clear = withDelete ? deleteSecret("session") : Promise.resolve();
  return clear.then(() => readSecret("session")).then((left) => {
    console.log(withDelete ? "with delete:" : "without delete:");
    console.log("store: " + (left === null ? "empty" : "full"));
    countFates(reconcile(signedIn, signedOut));
  });
}

saveSecret("session", "t-1")
  .then(() => signOut(false))
  .then(() => signOut(true));
```

```text
without delete:
store: full
kept: 3
updated: 0
mounted: 1
unmounted: 3
commands: 4
with delete:
store: empty
kept: 3
updated: 0
mounted: 1
unmounted: 3
commands: 4
```

In both runs the command count is the same: the account subtree is unmounted and `Login` is mounted. The difference is only in the store: in the first the token stays full, in the second it is empty. Someone looking at the tree cannot say that the session has really closed; if the token stays in the store, the app builds the session again at the next launch. The fate classifies what the update does to the node; it does not classify the fate of the token.

#### Common Misconceptions

- "The session closed and the screen changed, so the token was deleted." The screen is drawn from the state, not from the store. Deleting is a separate call and is verified separately.
- "As long as a secret value is visible on the screen it has entered memory, so the store is meaningless." The copy in memory is temporary; the store determines where the value remains when the app closes.
- "A token can sit in a plain store too, in the end it is a small piece of text." Size does not determine security; what protects access is the store itself.
- "If the deletion succeeded, the user has left the session." Deleting from the store and removing the session screen from the tree are separate jobs. Deleting produces no command in the tree; what changes the screen is the token leaving the state. If the two steps are not done together, the screen keeps showing the account while the store is empty, or the old token stays in the store while the screen has returned to login.
- "When the token is renewed, the screen is renewed too." A renewal produces no command in the tree; because the token is not visible on the screen, no node changes.
- "The secure store is only for tokens." Passwords, keys, and other secret values that belong to the user are written to the same place. What decides the choice is not the shape of the value but the harm that would arise if it were disclosed.
- "When the session closes, only the token goes." Everything belonging to the account goes from the tree, and the native view tied to it goes with it. The next user does not see the previous user's screen state; this is a wanted result and shows in the measurement as the subtree being unmounted as a whole.

#### Summary

- This lesson's fate: unmounted. When the session closed, `/App/Account` and the `Profile` and `Orders` nodes under it were unmounted as a whole, and `Login` was mounted in its place.
- When the token read from the secure store at launch changes the state, `Login` is unmounted and the account subtree is mounted.
- Deleting the token from the store does not change the tree: as long as the state stays in place, the `Account` node is kept.
- The same session closing can leave the store full or empty; the tree changes the same way in both cases, and the fate does not classify the fate of the secret value.
- A secret value sits not in a plain store but in the platform's secure key store.

#### Next Step

Until this lesson, the changes in the tree came from state: a screen opening, data loading, a session closing. There is also a change that comes from outside: a user who taps a notification opens the app at a specific screen. The next lesson, `Notifications and Deep Linking`, measures how a link enters the tree.

### Notifications and Deep Linking

Source: https://academia.sh/en/courses/react-native/notifications-and-deep-linking

Tapping a notification or opening a link starts the app at a specific screen. The lesson measures that in a running app a link builds a single screen on the stack, while in a closed app it builds the whole tree.

In the previous lessons the change in the tree came from inside the app: a tap, a response, a session. A change also comes from outside. The user taps a notification or clicks a link, and the app has to open on the screen of order 42. This lesson covers how an address that arrives from outside is converted to a route list and what the app being open or closed makes a difference to in the tree.

The question of the course does not change: **what does this update do to the node in the view tree?** The fate is kept, updated, mounted, or unmounted; the total of the updated, mounted, and unmounted nodes is the commands. We do not assign a fate by hand; the rule gives it:

```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 model builds this fate's rule; the model is not the framework itself. The rule does not see where the link came from; it compares only the tree before and the tree after.

#### A Link Becomes a Route

A deep link is an address that points to a specific screen of the app. A notification carries this address, a link in an email or another app opens it. A link is not a screen: the address is a piece of text, and the app's route table converts it to a route. The platform delivers the link in two ways. If the app is open, the link reaches the running app as an event. If the app is closed, the link is taken as the first address that starts the app.

```swift
// Platform code: NotificationTap
func userNotificationCenter(_ center: UNUserNotificationCenter,
                            didReceive response: UNNotificationResponse,
                            withCompletionHandler completionHandler: @escaping () -> Void) {
    let link = response.notification.request.content.userInfo["link"] as? String
    pendingLink = link
    completionHandler()
}
```

The platform side catches the link and passes it to the JavaScript side. In the framework there are two entries: asking for the first address at launch and listening for the event that arrives while the app is open.

```jsx
// Platform code: LinkListener
function useLink(onLink) {
  useEffect(() => {
    Linking.getInitialURL().then((url) => url && onLink(url));
    const sub = Linking.addEventListener("url", (e) => onLink(e.url));
    return () => sub.remove();
  }, []);
}
```

These two fragments are not measured; the model does not run the platform or the framework.

**AL1.** The model builds the link as a single address in the form `order/<id>`. `openLink(routes, link)` takes the current route list and the address and returns the new route list. If the address is not recognized, or the target route is already at the top of the stack, the list is returned as it is.

```javascript
// link.mjs — converts a deep link to a route list
export function openLink(routes, link) {
  const parts = link.split("/");
  if (parts[0] !== "order" || parts.length !== 2) return routes;
  const target = "order-" + parts[1];
  if (routes[routes.length - 1] === target) return routes;
  return [...routes, target];
}
```

**AL2.** We build the stack's tree from the routes as in the first lesson: under `Stack`, a keyed `Screen` for each route and a `Text` inside it.

```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)));
}
```

#### When the App Is Open

The user went down from the order list to the detail of an order, and then a notification arrived. The stack has `list` and `detail`. When the notification is tapped, the app is already running and the link arrives as an event.

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

const routes = ["list", "detail"];
const before = renderStack(routes);
const after = renderStack(openLink(routes, "order/42"));
const ops = reconcile(before, after);
printOps(ops);
countFates(ops);
```

```text
kept: /Stack
kept: /Stack/Screen[list]
kept: /Stack/Screen[list]/Text
kept: /Stack/Screen[detail]
kept: /Stack/Screen[detail]/Text
mounted: /Stack/Screen[order-42]
mounted: /Stack/Screen[order-42]/Text
kept: 5
updated: 0
mounted: 2
unmounted: 0
commands: 2
```

`/Stack/Screen[order-42]` is **mounted**, and with it the `Text` under it is mounted. The other screens in the stack and the texts inside them are **kept**. If the user goes back, they find the place where they left off; the link did not reset the stack, it put a screen on top of it.

#### When the App Is Closed

If the notification is tapped while the app is closed, there is no tree in memory. The platform starts the app and the first address is the link. The initial route is `list`, the link target is `order-42`; the stack is built with both.

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

const start = openLink(["list"], "order/42");
const ops = reconcile(null, renderStack(start));
printOps(ops);
countFates(ops);
```

```text
mounted: /Stack
mounted: /Stack/Screen[list]
mounted: /Stack/Screen[list]/Text
mounted: /Stack/Screen[order-42]
mounted: /Stack/Screen[order-42]/Text
kept: 0
updated: 0
mounted: 5
unmounted: 0
commands: 5
```

Because there is no previous tree, the whole tree is mounted: the stack, two screens, and two texts. There is no kept node, because there is nothing to keep. The same link does two different jobs in the tree depending on whether the app is open or closed: when open it mounts a single screen, when closed it mounts everything. If these two measurements are not kept apart, the cost of a link to the tree is misunderstood.

#### Reading the Platform Fragments Line by Line

The `NotificationTap` function catches the moment the user taps the notification. The function reads the address from a field inside the notification and writes it to a variable; it draws nothing on the screen and adds no route to the stack. The last line tells the platform that the work is done. The job of this function is only to leave the address: converting the address to a route and writing it to the stack is the app's next step.

This division is important. The platform code knows that a link has arrived but does not know which screen will open; that knowledge is in the route table. The route table, in turn, does not know where the link came from. For this reason the two parts can change independently: when the form of the notification changes, the platform code is updated; when the screen names change, the route table is updated.

The `LinkListener` fragment shows that the address reaches the JavaScript side in two ways. The first call asks for the first address if the app was started with a link; if there is no address, nothing happens. The second call listens for addresses that arrive while the app is running. The cleanup function (`return () => sub.remove();`) removes the listener, so that when the screen's lifetime ends, the listening ends too. Both entries are tied to the same function, `onLink`. The model reflects this commonality with `openLink`: the open and closed paths use the same conversion, and the only difference is whether there is a stack in hand beforehand.

#### Going Back

The user closes the screen opened by the link. The last route leaves the stack, and the screen the link added goes from the tree. We measure this too, because a link adds a screen to the stack, but the lifetime of this screen does not end with the link.

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

const ops = reconcile(
  renderStack(["list", "detail", "order-42"]),
  renderStack(["list", "detail"]),
);
printOps(ops.filter((op) => op.fate !== "kept"));
countFates(ops);
```

```text
unmounted: /Stack/Screen[order-42]
unmounted: /Stack/Screen[order-42]/Text
kept: 5
updated: 0
mounted: 0
unmounted: 2
commands: 2
```

The screen the link mounted and the text inside it are unmounted; the remaining five nodes are kept. Unmounting, the reverse of mounting, produces the same number of commands as mounting. The cost of a link to the tree is paid once for as long as the screen lives and once more when the screen closes: one mount, one unmount.

#### A Second Link

The user may have gone to an order and, without going back, tapped a second notification. The stack has `list` and `order-42`, and the new link carries the address `order/43`.

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

const routes = ["list", "order-42"];
const ops = reconcile(renderStack(routes), renderStack(openLink(routes, "order/43")));
printOps(ops.filter((op) => op.fate !== "kept"));
countFates(ops);
```

```text
mounted: /Stack/Screen[order-43]
mounted: /Stack/Screen[order-43]/Text
kept: 5
updated: 0
mounted: 2
unmounted: 0
commands: 2
```

The second link did not remove the first from its place; it put a new screen on top of it. `order-42` is kept, `order-43` is mounted. The model made this choice with `AL1`: if the address is recognized and the target is not at the top, a route is added. A real app could in this case choose to replace the old order; the measurement would then give a different fate, and this lesson does not measure that choice. What is measured is the behavior the model built.

#### What the Route Table Does

The route table is a small set of rules that bind addresses to route names. In our model this rule is a single line: an address in the form `order/<id>` becomes the route `order-<id>`. In a real app the table is longer, and each line defines a screen and sometimes the parameters of that screen. The table has two limits. For an address it does not recognize it produces nothing, which this lesson will show with the `unrecognized` measurement below. For an address it does recognize it produces only the route name; the content of the screen, that is, the order itself, comes from the network layer after the screen opens.

This second limit matters for the tree. The link mounts the screen, but the details of the order are not there yet. The screen first opens in the loading state, and then is updated when the data arrives. The cost of a link to the tree is therefore in two stages: the mounting of the screen and the arrival of the content. In this lesson only the first stage was measured; the second stage is the subject of the Networking and Data lesson.

#### Launch Order

When the app is started with a link, there are two paths. In the first, the app asks for the first address and builds the tree directly with the stack the link points to; our measurement showed this path, and five nodes out of five were mounted. In the second, the app first builds the initial screen, learns the address later, and handles the link like an event that arrives in a running app. On this path, the three nodes of the initial stack (`Stack`, the `list` screen, and its text) are mounted first, and the two nodes of the link are added later; the total is again five mounted nodes, but the user sees only the initial screen for a brief moment.

The two paths are equal in terms of the number of commands. The difference between them is a difference not of the tree but of the intermediate state the user sees. The rule does not see the intermediate state; it sees only the two trees being compared. For this reason comparing these two paths is a design evaluation outside the tree fate.

#### The Bounding Measurement

A link reaching the app does not mean the tree changes. The route table may not recognize the address, or the notification may arrive twice. In both cases the link has been processed but the tree does not change.

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

function measure(label, routes, link) {
  console.log(label);
  countFates(reconcile(renderStack(routes), renderStack(openLink(routes, link))));
}

measure("unrecognized:", ["list", "detail"], "campaign/7");
measure("duplicate:", ["list", "order-42"], "order/42");
```

```text
unrecognized:
kept: 5
updated: 0
mounted: 0
unmounted: 0
commands: 0
duplicate:
kept: 5
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

In both measurements the commands are 0: all nodes are kept. The fate does not measure whether the link was processed; it measures what became of the tree. A notification may have been tapped, the app may have received the link, and the route table may have made a decision; no trace may remain in the tree.

#### Common Misconceptions

- "A link arrived, so a new screen opened." A screen opens if a route is added to the route list. An unrecognized or a duplicate link does not change the list.
- "A notification always starts the app from scratch." If the app is open, the link arrives as an event and the stack is kept; a build from scratch happens only when the app is closed.
- "The notification itself is visible in the tree." The notification stays in the operating system's notification area; only the link that arrives when it is tapped enters the tree.
- "If the same notification arrives twice, two screens open." If the target route is already at the top, the model does not change the list. The duplicate link measurement gave 0 commands.
- "An unrecognized link gives an error." For an unrecognized address the model returns the list as it is and the tree does not change. If something is to be told to the user, this must be written to the tree as a separate node; then the measurement would show one mounted node instead of 0 commands.
- "A link always grows the stack." A link may add a route to the stack, may add nothing, or an app may choose to build the stack from scratch. This lesson measured only the adding behavior.
- "The notification permission shows the link in the tree." The permission determines whether the notification will be shown. Unless the notification that is shown is tapped, no link reaches the app and the tree does not change.
- "The same link always gives the same tree." The result depends on the stack at the moment the link arrives. The same `order/42` address produced five mounted nodes when the app was closed and there was no previous tree, and two mounted nodes in a stack with the `list` and `detail` screens. It is not possible to state the cost of a link by looking at the address alone.

#### Summary

- This lesson's fate: mounted. When the app is open, the link mounts the `/Stack/Screen[order-42]` screen, and the screens below it are kept.
- When the app is closed there is no previous tree; the link makes the whole tree mounted.
- The same link mounts a single screen when the app is open and the whole tree when it is closed; the two cases are measured separately.
- An unrecognized and a duplicate link produce no commands in the tree; a link having been processed does not mean the tree changed.
- The platform code that carries the link as an address and the route table that converts it to a route are two separate jobs; the result of the second determines the fate.

#### Next Step

Up to this lesson the tree changed in discrete steps: a tap, a response, a link. While a finger is dragged across the screen, the change happens in every frame. The last lesson, `Gestures and Animations`, measures how the command separates when the same movement is written to the tree in two ways.

### Gestures and Animations

Source: https://academia.sh/en/courses/react-native/gestures-and-animations

If a gesture's position is written to the tree as a prop, every frame produces an update command. If the gesture is driven on the native side, the tree is kept in the frames between. The lesson measures the two paths on the same gesture.

The previous lesson covered a change that comes from outside. A change also comes from a finger: a card is dragged, a panel slides open. A gesture is not a single event but a change that proceeds frame by frame. The card's position changes in every frame, and the question is this: does every step of this change pass through the view tree, or does the tree see only the start and the end?

The question of the course does not change: **what does this update do to the node in the view tree?** The fate is kept, updated, mounted, or unmounted; the total of the updated, mounted, and unmounted nodes is the commands. We do not assign a fate by hand; the rule gives it:

```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 model builds this fate's rule; the model is not the framework itself. The rule compares not frames but pairs of trees; how many frames a gesture lasts, we understand from how many times it visited the tree.

#### When the Position Is Written to the Tree

The first way to drive a gesture is to hold the position in the app's state. As the finger moves, the state changes, the tree is rebuilt, and the card's position prop carries the new value. For each frame two trees are compared, and the difference goes to the native side as commands.

**AL1.** The model builds the card as a single `Card` inside `Deck`; `Card` carries the position as a prop and has a `Text` under it. The position is given with numbers such as 0, 40, 80; the numbers are the identity of the frame, not a measure.

```javascript
// deck.mjs — builds the card deck's view tree for the two drive paths
import { el } from "./fate.mjs";

export function renderDeck(position) {
  return el("Deck", {}, [
    el("Card", { position: position }, [el("Text", { text: "Order" })]),
  ]);
}

export function renderDriven() {
  return el("Deck", {}, [
    el("Card", { driver: "native" }, [el("Text", { text: "Order" })]),
  ]);
}
```

`renderDeck` writes the position as a prop. `renderDriven` does not write the position to the tree; the card only reports that the gesture is driven on the native side. In the framework the second path is built by handing the gesture over to the native driver.

```jsx
// Platform code: SlideCard
function SlideCard({ x }) {
  useEffect(() => {
    Animated.timing(x, { toValue: 160, useNativeDriver: true }).start();
  }, []);
  return <Animated.View style={{ transform: [{ translateX: x }] }} />;
}
```

This fragment is not measured; the model does not run the native driver. The hook in the fragment does not pass the position through state: the start and end values of the gesture are given to the native side once, and the native side produces the frames in between.

The first measurement is a single frame transition in which the position is written to the tree: the card goes from position 0 to position 40.

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

const ops = reconcile(renderDeck(0), renderDeck(40));
printOps(ops);
countFates(ops);
```

```text
kept: /Deck
updated: /Deck/Card
kept: /Deck/Card/Text
kept: 2
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

`/Deck/Card` is **updated**, because the position prop changed. `Deck` and `Text` are **kept**; the text under the card is not affected by the gesture. The number of commands is 1: a single frame carried a single update.

#### One Command per Frame

A gesture does not end in a single frame. The same transition is repeated in every frame, and every repetition adds a command. The helper below compares consecutive frames and returns the command count of each transition.

```javascript
// frames.mjs — gives the command count of each transition between consecutive frames
import { reconcile } from "./fate.mjs";

export function stepCosts(frames, render) {
  const costs = [];
  for (let i = 1; i < frames.length; i++) {
    const ops = reconcile(render(frames[i - 1]), render(frames[i]));
    costs.push(ops.filter((op) => op.fate !== "kept").length);
  }
  return costs;
}
```

Two position sequences, one with three frames and one with four, are passed through the path that writes the position to the tree. For comparison, the same sequences are also passed through the natively driven path.

```javascript
// main.mjs
import { stepCosts } from "./frames.mjs";
import { renderDeck, renderDriven } from "./deck.mjs";

console.log("position in tree, 3 frames: " + stepCosts([0, 40, 80], renderDeck).join(" "));
console.log("position in tree, 4 frames: " + stepCosts([0, 40, 80, 120], renderDeck).join(" "));
console.log("natively driven, 3 frames: " + stepCosts([0, 40, 80], renderDriven).join(" "));
console.log("natively driven, 4 frames: " + stepCosts([0, 40, 80, 120], renderDriven).join(" "));
```

```text
position in tree, 3 frames: 1 1
position in tree, 4 frames: 1 1 1
natively driven, 3 frames: 0 0
natively driven, 4 frames: 0 0 0
```

As long as the position is in the tree, every transition is one command, and as the gesture gets longer the number of commands grows with the number of frames. In the natively driven path the cost of every transition is 0; the length of the gesture does not show in the tree.

#### A Natively Driven Gesture

The native driver advances the gesture from start to finish by itself. The tree knows only that the card exists and that the gesture is driven on the native side. In the frames between, the tree stays the same.

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

const ops = reconcile(renderDriven(), renderDriven());
printOps(ops);
countFates(ops);
```

```text
kept: /Deck
kept: /Deck/Card
kept: /Deck/Card/Text
kept: 3
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

`/Deck/Card` is **kept**. Even though the card is sliding on the screen, nothing happens for the tree: all three nodes are kept, commands is 0. The smoothness of the gesture is the native side's job; the position prop never enters the tree, so there is no need to compare again for each frame.

#### Reading the Source Fragment Line by Line

In the `SlideCard` fragment, the whole gesture fits in one call. The component takes an animation value `x`: the start position is inside this value and is not held in state. The `useEffect` block is a side effect that runs once at launch; it starts the gesture. `toValue` in the call gives the end position, and `useNativeDriver: true` says that the frames in between will be produced on the native side. The `return` line is the drawing and binds the position to the card as a transform.

What is missing from the fragment is also instructive: there is no state call that updates the position. While the gesture proceeds, the app's state does not change, so the tree is not rebuilt. `renderDriven` in the model is the counterpart of this absence in the tree: the card carries no position, it only reports that the gesture is driven on the native side. A different way of writing that holds the position in state would correspond to `renderDeck` and would require a call that changes the state for every frame.

The choice between these two ways of writing has a single condition: whether the app logic needs the gesture's intermediate values. If the logic will read the intermediate position, for example if something else will happen when the card passes a certain point, the value is held in state. If only a visual slide is needed, the native driver is enough.

#### More Than One Card

On a real screen, a single card does not slide at a time. When a deck opens, several cards move together. If the position is written to the tree, each card's position is a separate prop, and all of them are updated in every transition. We measure this by changing the number of cards.

```javascript
// main.mjs
import { el } from "./fate.mjs";
import { stepCosts } from "./frames.mjs";

function renderMany(count, driven) {
  return (position) => el("Deck", {}, Array.from({ length: count }, (_, i) =>
    el("Card", driven ? { driver: "native" } : { position: position },
      [el("Text", { text: "Order" })], "c" + i)));
}

const frames = [0, 40, 80];
console.log("1 card, position in tree: " + stepCosts(frames, renderMany(1, false)).join(" "));
console.log("2 cards, position in tree: " + stepCosts(frames, renderMany(2, false)).join(" "));
console.log("3 cards, position in tree: " + stepCosts(frames, renderMany(3, false)).join(" "));
console.log("3 cards, natively driven: " + stepCosts(frames, renderMany(3, true)).join(" "));
```

```text
1 card, position in tree: 1 1
2 cards, position in tree: 2 2
3 cards, position in tree: 3 3
3 cards, natively driven: 0 0
```

The cost of every transition equals the number of cards: 1 for one card, 2 for two cards, 3 for three cards. The total cost is therefore the product of two factors: the number of frames and the number of cards. In the natively driven path, even if all three cards slide, the cost is zero. The more nodes a gesture involves, the more the price of writing the position to the tree grows.

This product explains why a gesture can be expensive, but it gives no threshold. How many cards count as "many" depends on the app and the platform; this lesson measures only which factors the cost depends on.

#### Counterexample: Where the Position Is Needed in the Tree

The native driver is not always the right solution. If the card's position determines the state of another node, for example if a drop area is to be highlighted when the dragged card comes over it, the app needs to know the intermediate position. The native driver does not pass this information to the app in every frame. In this case either the position is held in state and the price is paid, or an event is sent to the app only at the moment of the threshold. The second option writes only that moment to the tree: 0 commands throughout the gesture, one update at the moment of the threshold.

In this choice the fate of the tree does not make the decision; the function of the gesture makes it. The fate only puts the prices of the two paths side by side.

#### The Bounding Measurement

It would be wrong to say that a natively driven gesture never visits the tree. When the gesture ends, the app needs to know the card's final position: whether the card has settled in place, been deleted, or closed. This information is written once from the state to the tree.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderDeck, renderDriven } from "./deck.mjs";

const ops = reconcile(renderDriven(), renderDeck(160));
printOps(ops);
countFates(ops);
```

```text
kept: /Deck
updated: /Deck/Card
kept: /Deck/Card/Text
kept: 2
updated: 1
mounted: 0
unmounted: 0
commands: 1
```

This time the card is updated: the driver prop went and the position prop came. The number of commands is 1. Throughout the gesture 0 commands, at the end 1 command. The fate does not measure whether the gesture is smooth; it measures what was written to the tree. A natively driven gesture has no commands in the tree, but the position written at the end is still an update.

#### Common Misconceptions

- "Every animation is driven natively, so the tree never changes." The driver is a choice. If the position is held in state, every frame enters the tree.
- "If commands is 0, the gesture is smooth." The command count measures the writing between the tree and the native side; it does not measure smoothness.
- "When the gesture ends, the tree stays the same." If the final position is needed by the app, it is written once, and this is a single update.
- "The number of cards does not change the tree cost." If the position is written to the tree, each card is a separate update. In the measurement with more than one card, the commands per transition came out equal to the number of cards.
- "Using the native driver complicates the code, and the tree is fast enough anyway." This is a performance claim, and the lesson does not measure it. What is measured is only the number of commands written to the tree; tree speed or frame time was not measured.
- "Driving a gesture from state is wrong." If logic depends on the intermediate position, driving from state is the right choice. What is wrong is writing every frame to the tree when there is no need.
- "A gesture's state and the tree are the same thing." The animation value is the number inside the gesture; the tree is the screen's state at that moment. The native driver advances the number but does not write it to the tree. This separation of two different things explains the zero commands in the measurements.
- "More than one gesture shares the same command." The position of each card written to the tree is a separate prop; the commands multiply by the number of cards. No sharing was seen in the multi-card measurement.
- "The duration of the animation determines the number of commands." Duration was not measured in this lesson. What was measured is the number of transitions: when the position is in the tree, every frame brings a transition and a command; how the duration converts to a number of frames depends on the platform and is not part of this lesson.
- "The native driver is only for scrolling." The driver can also be used for other visual props such as a transform, besides position. The common condition is that the intermediate values are not needed by the app logic.
- "A gesture and a transition are the same thing." A gesture is the continuous flow of many frames; a transition is a single comparison between two trees. The lesson measures the gesture by dividing it into transitions, and this division explains how the tree rule sees the gesture.
- "Removing the card from the tree stops the gesture." If the card is unmounted, the gesture also disappears; but this is a different fate. Removing the card in the middle of a gesture produces an unmounted node in the tree, and this was not the subject of the measurement.

#### Summary

- This lesson's fate: kept. In a natively driven gesture, `/Deck/Card` is kept in the frames between and commands is 0.
- If the position is written to the tree as a prop, every frame updates the `/Deck/Card` node and the number of commands grows with the number of frames.
- In a natively driven gesture, the number of commands does not change as the number of frames grows.
- Writing the position to the tree once when the gesture ends is a single update command.
- The fate classifies not the smoothness of the gesture but what is written to the tree.

#### Next Step

With this lesson the layers of the app are complete: navigation, state, network, storage, notifications, and gestures. Each lesson measured the fate of a change in the tree. In the lessons that follow, the same measure returns to performance questions. The first lesson of the next section, `List Performance`, measures what happens to the row nodes when an item is added to the head of a list.

## Performance and Production

Virtualized list settings, frame rate and profiling, bundle size and startup, debugging tools, testing, writing native modules, build and release.

### List Performance

Source: https://academia.sh/en/courses/react-native/list-performance

Measures what happens to the row nodes when an item is added to the front of a list. With index matching, the contents of the rows shift; with a stable key, the same insertion yields a single mounted node.

In the previous lesson, the animation ran on the native side. The advancing value never entered the tree as a prop, so the node stayed **kept**. A list is the opposite case: what changes is the elements of the tree themselves, and a single insertion can affect the fate of every row. This lesson does not ask how fast a list scrolls. It asks what happens to each row node when an item is added.

The course's question is the same: **what does this update do to the node in the view tree?** The answer has four forms. A node that stays in place with the same props is **kept**; if its props changed, it is **updated**. A node with no counterpart in the previous tree is **mounted**, and a node left with no counterpart in the new tree is **unmounted**. The work an update sends to the native side is the sum of the three fates other than kept, and that sum is called the **commands**. The rule below gives the fate, not a label. Producing the tree again from state is called a render. The rule compares two trees by matching their children by key, or by position when there is no key.

```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 part of the rule that matters in this lesson is the `identity` function. A child with a key is recognized by its key, and a child without a key by its position. Children carrying the same identity in the two trees form a pair; if the pair has the same type and key, their props are compared.

The function that turns the list into a tree lives in a separate file. `renderInbox` turns the items into row nodes. If its second parameter is `true`, it gives each row the item id as a key; if `false`, the row stays without a key.

```javascript
// inbox.mjs — turns an inbox into a view tree
import { el } from "./fate.mjs";

export const ITEMS = [
  { id: "a", title: "Meeting" },
  { id: "b", title: "Invoice" },
  { id: "c", title: "Invite" },
];

export const FRESH = { id: "d", title: "Announcement" };

export function renderInbox(items, useKeys) {
  const rows = items.map((item) =>
    el("Row", { title: item.title }, [], useKeys ? item.id : null));
  return el("Inbox", {}, rows);
}
```

The inbox has three items, and a fourth, `Announcement`, arrives at the front of the list. Seeing the new item at the top is the behavior the user expects: the newest message stays at the top.

#### Matching by Index

In the first measurement, the rows are given no keys. The old tree has three rows and the new tree has four. The rule matches the rows by position: the first row of the old tree pairs with the first row of the new one, the second with the second, the third with the third. The fourth row of the new tree has no counterpart in the old one.

```javascript
// main.mjs
import { reconcile, printOps } from "./fate.mjs";
import { renderInbox, ITEMS, FRESH } from "./inbox.mjs";

const before = renderInbox(ITEMS, false);
const after = renderInbox([FRESH, ...ITEMS], false);
printOps(reconcile(before, after));
```

```text
kept: /Inbox
updated: /Inbox/Row
updated: /Inbox/Row
updated: /Inbox/Row
mounted: /Inbox/Row
```

Three rows came out **updated** and the last row came out **mounted**. The added item was at the front, but the new row in the tree was born at the end. The first row used to show `Meeting` and now shows `Announcement`; the second row shows `Meeting` where it showed `Invoice`, and the third row shows `Invoice` where it showed `Invite`. No row was mounted to represent the new item: the existing rows shifted the items down by one place and the surplus was added at the end. Because the props of three rows changed, three updates went out. The root node `/Inbox` has no props and stayed **kept**.

The paths of the rows without keys are all the same, `/Inbox/Row`, because a path is built only from the type and the key. The output does not say which row is which; the order does. Inside the rule, each one is recognized by the identity `#0`, `#1`, `#2`.

This is not only extra commands. In a real list, a row also has state of its own: being selected, a detail left open, a half-written reply. With index matching, the row node is kept but moves on to another item. The state stays in the row while the item shifts; the half-written reply in the first row suddenly becomes the reply to the `Announcement` item. What is called identity preservation breaks here: a kept or updated node carries on its identity, and the identity is now attached to the wrong item.

#### Stable Key

The second measurement makes the same insertion, but the rows are given the item id as a key. A key recognizes an item independently of its position: item `a` stays `a` whatever place it takes in the list. Such a key is called a stable key.

```javascript
// main.mjs
import { reconcile, printOps } from "./fate.mjs";
import { renderInbox, ITEMS, FRESH } from "./inbox.mjs";

const before = renderInbox(ITEMS, true);
const after = renderInbox([FRESH, ...ITEMS], true);
printOps(reconcile(before, after));
```

```text
kept: /Inbox
mounted: /Inbox/Row[d]
kept: /Inbox/Row[a]
kept: /Inbox/Row[b]
kept: /Inbox/Row[c]
```

Now a single row was mounted: the new item's own row, `/Inbox/Row[d]`. The three old rows matched by their keys, and since their props stayed the same, all of them came out **kept**. The new row is at the front only by its position in the tree; the output of the rule prints it first as well. Identity preservation is also correct this time: the row of item `a` continues as the same row, together with the state inside it.

The key must be stable. If the position in the list is given as the key, the key is tied to the position and the situation of the first measurement returns. If the key is regenerated at every update, for example from a random number, every row loses its old identity each time; all the old rows come out unmounted and all the new rows mounted.

In the real framework, the key is derived from an item id in the function that produces the row. In a virtualized list, this is the job of the key extractor function. The snippet below shows the source form of the list; the model does not run this snippet.

```jsx
// Platform code: Inbox
function Inbox({ items }) {
  return (
    <FlatList
      data={items}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <Row title={item.title} />}
    />
  );
}
```

`keyExtractor` extracts a key from each item; the model represents this with the `useKeys` parameter. If no key extractor is defined, the real list may choose this fallback behavior on its own; the model measures only the two ends, with no keys and with keys.

#### Deleting from the Middle

Since the insertion was made at the front, the shift was the extreme case. Deletion belongs to the same class, and an item deleted from the middle separates the two matchings of the rule even further. Item `b`, `Invoice`, is removed from the list. The two remaining items, `a` and `c`, stay where they are.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderInbox, ITEMS } from "./inbox.mjs";

const remaining = ITEMS.filter((item) => item.id !== "b");
for (const useKeys of [false, true]) {
  console.log(useKeys ? "keyed matching" : "index matching");
  countFates(reconcile(renderInbox(ITEMS, useKeys), renderInbox(remaining, useKeys)));
}
```

```text
index matching
kept: 2
updated: 1
mounted: 0
unmounted: 1
commands: 2
keyed matching
kept: 3
updated: 0
mounted: 0
unmounted: 1
commands: 1
```

With index matching, although the deleted item is `b`, the unmounted row is the last row: the second row was updated to show the `Invite` item, and the third row was left without a pair and was unmounted. With a stable key, the unmounted row is the deleted item's own row, `/Inbox/Row[b]`; the other rows are kept. The command count dropped from two to one.

The common lesson of these two measurements is this: in a list without keys, the tree cannot tell which item was added or removed; it can only tell by how many rows the end grew or shrank. A key adds the identity of the item to the tree. If the list items can be sorted, filtered, or reordered by the user, this identity is not optional at all.

#### Window and Virtualization

The rule sees all the items of the list in the tree. A real virtualized list does not work that way. In a list of thousands of items, native views are mounted only for the rows visible on the screen and their immediate surroundings; this range is called the window. As the user scrolls, the rows leaving the window are unmounted on the native side and the rows entering it are mounted. For this reason, the performance of the list depends on the width of the window more than on the number of rows in the tree.

The settings of the window are written in the source of the list. If the height of a row is known in advance, the list can compute its position without mounting it; the window width and the number of items to render at first launch are also settings.

```jsx
// Platform code: InboxTuned
function InboxTuned({ items }) {
  return (
    <FlatList
      data={items}
      keyExtractor={(item) => item.id}
      getItemLayout={(_, index) => ({ length: 64, offset: 64 * index, index })}
      windowSize={5}
      initialNumToRender={8}
      renderItem={({ item }) => <Row title={item.title} />}
    />
  );
}
```

The numbers written here are the settings of one list; they are not measurements. Which value works is shown by the profiler; the next lesson covers that tool. What matters in this lesson is that none of these settings is an input of the rule. The tree does not change while the window mounts and unmounts rows on the native side.

#### The Bounding Measurement

The two matchings are put side by side. The same insertion is made in two runs, and `countFates` prints the fate counts of each. What is compared is the command count, that is, the units of work going to the native side.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderInbox, ITEMS, FRESH } from "./inbox.mjs";

for (const useKeys of [false, true]) {
  console.log(useKeys ? "keyed matching" : "index matching");
  const before = renderInbox(ITEMS, useKeys);
  const after = renderInbox([FRESH, ...ITEMS], useKeys);
  countFates(reconcile(before, after));
}
```

```text
index matching
kept: 1
updated: 3
mounted: 1
unmounted: 0
commands: 4
keyed matching
kept: 4
updated: 0
mounted: 1
unmounted: 0
commands: 1
```

Index matching gave four commands, and keyed matching gave one command. For the same insertion, that is four commands against one. In a list of three items the difference looks small; it grows as the list gets longer, because with index matching every item added at the front shifts every row after it down by one place. With a stable key, the command count does not depend on the length of the list.

The focus node is the first row of index matching, `/Inbox/Row`. All the records carrying the same path are filtered.

```javascript
// main.mjs
import { reconcile, printOps } from "./fate.mjs";
import { renderInbox, ITEMS, FRESH } from "./inbox.mjs";

const ops = reconcile(renderInbox(ITEMS, false), renderInbox([FRESH, ...ITEMS], false));
printOps(ops.filter((op) => op.path === "/Inbox/Row"));
```

```text
updated: /Inbox/Row
updated: /Inbox/Row
updated: /Inbox/Row
mounted: /Inbox/Row
```

The first record is `updated`: the first row stayed in place but now shows another item. This record is the lesson's fate.

There are two things the rule does not see. The rule sees all the items in the tree, but not the window; in a real list, not all four of these commands may be needed on the native side, because the rows outside the window have no native view. The second is that the work a command holds on the native side is not in the model: changing the text of a row and reloading the image inside the row are the same number of commands. Telling the two apart falls to the profiler.

**PP1.** The model builds this fate's rule; the model is not the framework itself. In the model the list has three items, there is no window, and every item is a row node in the tree. A real list mounts native views only for the items inside the window, chooses its own fallback behavior if no key extractor is defined, and can memoize a row to skip the re-render of a row whose props did not change; the model builds none of these. The model matches a child without a key by position and a child with a key by its key, and it does not interpret the behavior between the two ends it measures.

#### Summary

- This lesson's fate: updated. With index matching, the `/Inbox/Row` node stayed in place when an item was added at the front, but was updated to show another item.
- With index matching, an item added at the front updated three rows and mounted one row at the end; the command count became four.
- With a stable key, the same insertion mounted a single `/Inbox/Row[d]` node, and the three old rows stayed kept; the command count became one.
- With index matching, the state of a row gets attached to the wrong item; a stable key keeps the identity together with the item.
- The rule sees all the items in the tree; it does not see the window or the native side's work.

#### Next Step

In this lesson the tree changed and the commands were counted. It has also been seen that a render does not always produce commands: with a stable key, three rows gave no commands even though the tree was produced again. The next lesson, `Frame Rate and Profiling`, measures that an update that produces the tree three times can give zero commands.

### Frame Rate and Profiling

Source: https://academia.sh/en/courses/react-native/frame-rate-and-profiling

Measures that an update which produces the tree three times can give zero commands. A re-render and a command are told apart; memoization lowers the counter, not the command.

The previous lesson showed that an item added at the front can bring the command count down from four to one. There the tree was changing, and the question was how the change was distributed among the rows. In this lesson the tree does not change: it is produced three times from the same state. The user can still experience a stutter, because producing the tree is also work. This lesson explains that this work does not show up in the tree, but it can be measured.

The course's question has not changed: **what does this update do to the node in the view tree?** The answers are kept, updated, mounted, and unmounted; the sum of everything other than kept is the commands. The rule is given below in the core file.

```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 function that turns the screen into a tree lives in a separate file. The screen has a header, a chart, and a footer note. Each time the function is called, it increments a lesson-local counter; the counter counts re-renders. The counter is not in the tree and is not an input of the rule: the rule only compares two trees.

```javascript
// screen.mjs — turns a screen into a view tree and counts re-renders
import { el } from "./fate.mjs";

export const counter = { renders: 0 };

export function renderScreen(state) {
  counter.renders += 1;
  return el("Screen", {}, [
    el("Header", { title: state.title }),
    el("Chart", { points: state.points }),
    el("Footer", { note: state.note }),
  ]);
}

export function memoRender(render) {
  let lastKey = null;
  let lastTree = null;
  return (state) => {
    const key = JSON.stringify(state);
    if (key !== lastKey) {
      lastKey = key;
      lastTree = render(state);
    }
    return lastTree;
  };
}
```

`renderScreen` turns the state into a tree. `memoRender` wraps a render function: if the state is the same as before, it does not produce the tree again and returns the previous one. This is memoization. The difference between a re-render and a command is measured with these two functions.

#### Three Re-renders

In the first measurement, the tree is produced three times from the same state. Three trees give two updates: from the first to the second, and from the second to the third. The rule accumulates the two updates in the same `ops` array and counts them; the re-render count is read separately from the counter.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderScreen, counter } from "./screen.mjs";

const state = { title: "Sales", points: [3, 5, 8], note: "daily" };
const first = renderScreen(state);
const second = renderScreen(state);
const third = renderScreen(state);

const ops = [];
reconcile(first, second, "", ops);
reconcile(second, third, "", ops);
console.log(`re-renders: ${counter.renders}`);
countFates(ops);
```

```text
re-renders: 3
kept: 8
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

Three re-renders, zero commands. Each of the four nodes came out kept in both updates, eight records. The tree was produced three times, but the trees produced were equal to each other, and no work came out to go to the native side.

In M10/K03, how many times a function ran was being counted. The counter here counts the same thing, but the fate does not look at that number. **A re-render is not a command; three re-renders can give zero commands.** A re-render is the production of the tree; a command is the work that goes to the native side after two trees are compared. The two can move in the same direction or in different directions: a re-render can give a hundred commands, or none at all.

#### Memoization

If the counter is three, it can be seen that the three are unnecessary: the state never changed. Memoization cuts this waste. The same three calls are made with the render function wrapped by `memoRender`; the counter starts from zero.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderScreen, memoRender, counter } from "./screen.mjs";

const render = memoRender(renderScreen);
const state = { title: "Sales", points: [3, 5, 8], note: "daily" };
const first = render(state);
const second = render(state);
const third = render(state);

const ops = [];
reconcile(first, second, "", ops);
reconcile(second, third, "", ops);
console.log(`re-renders: ${counter.renders}`);
countFates(ops);
```

```text
re-renders: 1
kept: 8
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

The re-renders dropped from three to one; the commands were zero and stayed zero. Memoization did work, but the work has no effect on the fate in the tree: the commands were already zero. The value of memoization is not in the tree but in the work of producing the tree itself. If the tree produced is large and expensive, memoization skips that work; the rule does not see this, because it says the two trees are equal in the same way in both cases.

The point to notice here is the key of the memoization. `memoRender` compares the state by its contents. In real frameworks, the comparison is usually made by the identity of the props, that is, whether they are the same object. A parent screen that produces a new array or a new function on every call defeats the memoization even if the contents are the same. This difference is stated in the assumptions block; the model does not measure it.

On a real screen, what is memoized is usually a heavy subtree, for example a chart. The snippet below writes the chart as a memoized function component, and the screen recomputes the array it gives to the chart only when the rows change. The model does not run this snippet.

```jsx
// Platform code: SalesScreen
const Chart = React.memo(function Chart({ points }) {
  return <ChartView data={points} />;
});

function SalesScreen({ state }) {
  const points = useMemo(() => state.rows.map((r) => r.total), [state.rows]);
  return <Chart points={points} />;
}
```

`React.memo` wraps the chart: if its props are the same as before, the chart is not re-rendered. `useMemo` rebuilds the `points` array only when `state.rows` changes; if the array stays the same, the wrapped chart sees no new prop.

#### Changing Data

To understand what memoization does, the opposite must be measured too: what happens when the data really changes? The points of the chart change, while the header and the footer note stay the same.

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

const before = renderScreen({ title: "Sales", points: [3, 5, 8], note: "daily" });
const after = renderScreen({ title: "Sales", points: [3, 5, 9], note: "daily" });
printOps(reconcile(before, after));
```

```text
kept: /Screen
kept: /Screen/Header
updated: /Screen/Chart
kept: /Screen/Footer
```

A single command went out: `/Screen/Chart` is updated. The header and the footer note stayed kept; the screen root has no props, so it is kept as well. When the data changes, a re-render is mandatory and a command is mandatory. The job of memoization is to skip the re-render when the data does not change; when the data changes, there is nothing for it to skip.

#### Profiling

A stutter comes not from the re-render count but from the time the re-render takes. For every frame, the screen has to prepare the new image within a certain time; if the work on the JS side does not fit into this time, a frame is dropped. A dropped frame is the stutter the user sees. This lesson writes no time, because time is not in the model; the measurement is made on a real device, with the profiler.

The profiler tells two things apart. The first is the work on the JS side: processing the state, producing the tree, comparing the two trees. The second is the work on the native side: applying the commands to the native views, computing the layout, drawing. A screen that gives zero commands can still stutter because of the work on the JS side; a screen that gives many commands may still be handled in time by the native side. The place called the hot path is where these two kinds of work are repeated the most from frame to frame: scrolling, typing, dragging.

The place measured by the profiler can be marked with a wrapper. The snippet below puts the chart inside a profiler wrapper and passes the time of every render to a collector. The model does not run this snippet.

```jsx
// Platform code: ProfiledChart
function ProfiledChart({ points }) {
  return (
    <Profiler id="chart" onRender={(id, phase, duration) => collect(id, duration)}>
      <Chart points={points} />
    </Profiler>
  );
}
```

The time this snippet gives is the render work of this chart on the JS side. It is not the same thing as the command count; the two are read together. If the time is high and the commands are zero, look for memoization. If the time is low and the commands are many, the tree itself is changing and the keys are reviewed.

#### Reading the Profiler Wrapper Line by Line

Each line of the snippet above does something.

- `<Profiler id="chart" ...>` opens the wrapper and gives the measurement its name. There can be more than one wrapper on the same screen; `id` says which one is speaking.
- `onRender={(id, phase, duration) => collect(id, duration)}` is the function called when each render finishes. `phase` tells whether the render is the first render, an update, or an update that started inside an update, and `duration` gives the JS-side work of this render. `collect` is the collector; it is the application's own function.
- `<Chart points={points} />` is the measured subtree. The wrapper measures only what is inside it; the header and the footer note do not enter this number.

It should be noted that the wrapper is not a node in the tree. The measuring apparatus does not change the view tree; this is why the model leaves the path `/Screen/Chart` the same. If a measuring tool adds a node to the tree, the tool is spoiling what it measures.

#### Common Misconceptions

The first misconception is that many re-renders mean a stutter. Since three re-renders can give zero commands, the number alone says nothing. A re-render is work processed only on the JS side; as long as nothing goes to the native side, no view on the screen changes. A stutter happens only when the work of the re-render does not fit into the frame time, and it is the profiler, not a count, that shows this.

The second misconception is that memoizing everything is safe. Memoization requires a comparison: `memoRender` turns the state into a key on every call and compares it with the previous one. If the state rarely stays the same, this comparison is wasted work and you still produce the tree. Memoization pays off where the state often stays the same; scattering it everywhere is not a gain but a cost.

The third misconception is that zero commands are always good. When the data really changes, zero commands would mean that the screen keeps showing the old data. In the Changing Data measurement, the chart coming out updated is not a defect but the correct behavior. What is good is the absence of unnecessary commands, not the absence of commands altogether.

#### The Bounding Measurement

In the last measurement, the header changes and the chart does not. Three re-renders are made, and two updates are counted. The header takes a new value in each update; the chart, the footer note, and the root stay the same. The rule shows the chart by filtering its path.

```javascript
// main.mjs
import { reconcile, printOps, countFates } from "./fate.mjs";
import { renderScreen, counter } from "./screen.mjs";

const points = [3, 5, 8];
const first = renderScreen({ title: "Sales", points, note: "daily" });
const second = renderScreen({ title: "Sales (week)", points, note: "daily" });
const third = renderScreen({ title: "Sales (month)", points, note: "daily" });

const ops = [];
reconcile(first, second, "", ops);
reconcile(second, third, "", ops);
console.log(`re-renders: ${counter.renders}`);
printOps(ops.filter((op) => op.path === "/Screen/Chart"));
countFates(ops);
```

```text
re-renders: 3
kept: /Screen/Chart
kept: /Screen/Chart
kept: 6
updated: 2
mounted: 0
unmounted: 0
commands: 2
```

The focus node is `/Screen/Chart`: kept in both updates. The chart entered the tree in each of the three re-renders and was compared three times with the previous chart, but since its props were the same, nothing went to the native side. The header came out updated in two updates; both of the commands went to the header.

The limit of this measurement is the real warning of the lesson. The chart was re-rendered three times, and a re-render is work: the points of the chart may have been processed three times, a heavy computation may have run three times. The rule does not see this. The rule sees only what shows up in the tree; a node coming out kept does not say that producing that node was cheap. A stutter can live behind a kept node. Finding it takes the profiler; a tree diff cannot find it.

**PP2.** The model builds this fate's rule; the model is not the framework itself. In the model, a re-render is a function producing the tree again, and the counter counts these calls; in the real framework, the state, the context, and the parent screen together determine under what condition a screen is re-rendered, and the model builds none of these. The `memoRender` in the model compares the state by its contents; real memoizations usually compare by object identity, and this difference is not measured. The frame time, the distribution of work between the JS side and the native side, and the dropped frame are not in the model.

#### Summary

- This lesson's fate: kept. The `/Screen/Chart` node stayed kept in both updates, even though the tree was produced three times and the header changed.
- The tree produced three times from the same state gave zero commands; a re-render is not a command.
- Memoization brought the re-render count from three to one, and the commands stayed zero; the value of memoization is in the work of producing the tree.
- When the points of the chart changed, `/Screen/Chart` came out updated and a single command went out; memoization skips nothing when the data changes.
- The cost of a stutter sits in the work on the JS side; the rule sees only what shows up in the tree, and the profiler measures this work.

#### Next Step

In this lesson, the tree was always the tree of the same screen, and the screen was there from the very beginning. Loading all the screens of the application at launch stretches this assumption: loaded code is also a cost. The next lesson, `Bundle Size and Startup`, measures when deferring the code of a screen mounts its node.

### Bundle Size and Startup

Source: https://academia.sh/en/courses/react-native/bundle-size-and-startup

When a deferred screen is loaded, the placeholder is unmounted and the screen's node is mounted. The lesson measures that lazy loading changes the time of mounting, not the fate.

The previous lesson regenerated the tree three times and no command came out. The screen was there from the start. In a real application, not every screen is there from the start, and the code of a screen does not have to be loaded from the start either. The application's JS code lives in a bundle; at launch, that bundle has to be read and run. As the bundle grows, the launch also carries the code of screens the user will never open.

The course's question: **what does this update do to the node in the view tree?** The answers are kept, updated, mounted, and unmounted; the sum of everything other than kept is the commands. The rule is in the core file.

```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}`);
}
```

In this lesson, loading the code of a screen is work outside the tree. When the code is loaded, a node enters the tree, and the fate looks at that node. Two files build this distinction. The first is the loader that loads the screen code on the first request and counts the loaded modules. Loading is deferred with a `Promise`; a module is loaded on the first request, and on later requests the same module is returned.

```javascript
// loader.mjs — loads screen code on first use and counts loaded modules
export const loaded = { count: 0 };

const cache = new Map();

const MODULES = {
  Reports: () => Promise.resolve({ title: "Reports", rows: 3 }),
  Settings: () => Promise.resolve({ title: "Settings", rows: 2 }),
};

export function loadScreen(name) {
  if (!cache.has(name)) {
    cache.set(
      name,
      MODULES[name]().then((mod) => {
        loaded.count += 1;
        return mod;
      })
    );
  }
  return cache.get(name);
}
```

`loadScreen` loads each module on the first request and increments the counter. On the second request, the promise in the cache is returned; the counter does not increment. `loaded.count` is not in the tree and is not an input of the rule; the lesson keeps it only to show the number of loaded modules.

The second file is the function that produces a tree from the state. The application has a header and a content area. If the reports screen has not been loaded, a placeholder stands in the content area; if it has been loaded, the reports screen stands there.

```javascript
// app.mjs — turns the app into a view tree
import { el } from "./fate.mjs";

export function renderApp(state) {
  const content = state.reports === null
    ? el("Placeholder", { text: "Loading" })
    : el("Reports", { rows: state.reports.rows });
  return el("App", {}, [el("Header", { title: "Dashboard" }), content]);
}
```

#### Deferred Screen at Launch

When the application launches, the reports screen has not been requested yet. The launch tree carries the placeholder; since the tree is built from scratch, `reconcile(null, ...)` gives every node as mounted. The number of loaded modules is zero.

```javascript
// main.mjs
import { reconcile, printOps } from "./fate.mjs";
import { renderApp } from "./app.mjs";
import { loaded } from "./loader.mjs";

const first = renderApp({ reports: null });
console.log(`loaded modules: ${loaded.count}`);
printOps(reconcile(null, first));
```

```text
loaded modules: 0
mounted: /App
mounted: /App/Header
mounted: /App/Placeholder
```

At launch, three nodes were mounted and no screen module was loaded. The code of the reports screen may sit in the bundle, but since it did not enter the tree and was not requested, it was no load at launch.

#### When It Loads

The user opened the reports screen. First the module is loaded, then the state is updated and the tree is produced again. In the new tree, there is no placeholder in the content area; there is the reports screen.

```javascript
// main.mjs
import { reconcile, printOps } from "./fate.mjs";
import { renderApp } from "./app.mjs";
import { loadScreen, loaded } from "./loader.mjs";

const first = renderApp({ reports: null });
const mod = await loadScreen("Reports");
const second = renderApp({ reports: mod });
console.log(`loaded modules: ${loaded.count}`);
printOps(reconcile(first, second));
```

```text
loaded modules: 1
kept: /App
kept: /App/Header
unmounted: /App/Placeholder
mounted: /App/Reports
```

The header and the root stayed kept. In the content area there were two separate types: the placeholder and the reports screen. Because the type changed, the rule unmounted the old one and mounted the new one without looking at the props. `/App/Placeholder` came out unmounted, and `/App/Reports` came out mounted. The reports screen is a node with no counterpart in the previous tree; it is not the next state of the placeholder.

Lazy loading changed the time of mounting here. The reports screen's node was not mounted at launch; it was mounted at the moment the user asked for it. The same node could also have been mounted at launch with eager loading. That is measured separately.

#### Eager Loading

With eager loading, the code of both screens is loaded before launch. The reports screen is already in the first tree; since the tree is built from scratch, `reconcile(null, ...)` again gives only mounted.

```javascript
// main.mjs
import { reconcile, countFates } from "./fate.mjs";
import { renderApp } from "./app.mjs";
import { loadScreen, loaded } from "./loader.mjs";

await Promise.all([loadScreen("Reports"), loadScreen("Settings")]);
console.log(`eager: loaded modules ${loaded.count}`);
const tree = renderApp({ reports: await loadScreen("Reports") });
countFates(reconcile(null, tree));
```

```text
eager: loaded modules 2
kept: 0
updated: 0
mounted: 3
unmounted: 0
commands: 3
```

With eager loading, launch loaded two modules and mounted three nodes; with lazy loading, launch loaded zero modules and mounted three nodes. The three mounted nodes are the same in both measurements, but one of them is different: in the eager one the third node is the reports screen, in the lazy one it is the placeholder. Lazy loading did not reduce the command count in the tree; it moved the load from launch to the moment of the first request. The cost of this is that the user sees the placeholder at the first request.

In M12/K04, the question was whether a piece ran in this request. The same question can be asked for the screen here, but the fate does not look at that question. **When a deferred screen is loaded, its node is mounted; "deferred" is not a fate.** A deferred screen has no place in the tree; until it is loaded, there is a placeholder. The placeholder's own fate is unmounted, and the screen's fate is mounted.

#### How Deferral Looks in the Source

In a real application, there are two ways to defer a screen. The first is to define the screen with a deferred import and to show a placeholder until the screen is requested. The model does not run this snippet.

```jsx
// Platform code: App
const Reports = React.lazy(() => import("./Reports"));

function App() {
  return (
    <Suspense fallback={<Placeholder />}>
      <Reports />
    </Suspense>
  );
}
```

`Suspense` holds the placeholder: as long as `Reports` is not ready, `Placeholder` stands in the tree, and when it is ready, `Reports` takes its place. This is the unmount and mount pair we saw in the measurement.

The second way is not to read the module until a button is pressed. Since the module is requested inside the function, running it is deferred until that point as well.

```jsx
// Platform code: OpenReports
function OpenReports({ onOpen }) {
  const open = () => {
    const mod = require("./Reports");
    onOpen(mod.default);
  };
  return <Button title="Reports" onPress={open} />;
}
```

Both ways leave the loading or running of the code to the first use. Both mean less work at launch, and both bring a wait at the first use. How long the wait lasts is not in the model; that time is measured on a real device and with the real bundle.

#### Reading the Source Snippets Line by Line

In the first snippet, three lines do the work. `React.lazy(() => import("./Reports"))` does not read the screen right away; it only sets up a definition that says "fetch this module when needed". `<Suspense fallback={<Placeholder />}>` says which node will stand in the tree until the screen is ready. `<Reports />` is the real node wanted in the tree; as long as it is not ready, it does not enter the tree, and the placeholder stands in its place. In the model, `loadScreen` corresponds to the first line, and `renderApp` to the second and third lines.

In the second snippet, `require("./Reports")` is inside the function, and so it runs only when `open` is called. `onOpen(mod.default)` hands the loaded screen to the application; `onPress={open}` ties the call to the user's touch. There is no tree-level placeholder here: until the button is pressed, neither the screen nor a placeholder is in the tree, and when it is pressed, the screen is mounted.

The difference between the two snippets changes the placeholder's fate. In the first way, a placeholder is in the tree and comes out unmounted when the screen loads. In the second way, the placeholder is never mounted; the screen is directly mounted. In both ways, the lesson's fate is the screen being mounted.

#### Why "Deferred" Is Not a Fate

It is useful to work the distinction above through with an example. At launch, since the screen has not been loaded, there is nothing in the tree at the path `/App/Reports`. The rule keeps no record for a node without a path; there is no record called "deferred" either. At the moment loading takes place, a new node appears in the tree and the rule sees it for the first time: mounted.

So deferral does not change the node's fate; it only changes when the node will enter the tree. The node of a deferred screen gets no fate until the update in which it is mounted.

#### Common Misconceptions

The first misconception is that deferral makes the bundle smaller. Deferral changes when the code will be loaded; it does not change whether it is in the bundle. The screen's code still enters the bundle, or comes alongside as a separate piece. Reducing the size is a separate job.

The second misconception is that deferring every screen is a gain. Deferral brings a wait at the first use. Deferring a screen the user will go to right away does not lighten launch; it only shifts the wait to another moment. The Eager Loading measurement shows this: when the code is loaded beforehand, the user sees no wait, but more work is done at launch.

The third misconception is that the placeholder is free in the tree. The placeholder is a node; it is mounted, and it is unmounted when the screen loads. Two commands go out at the moment of loading for this reason: one unmounts the placeholder, the other mounts the screen.

#### Bundle Size

The size of the bundle is made up of two things: the application's own code and the code it imports. To reduce the size, first look at what is in the bundle. An import that is never used takes up space in the bundle; so does importing a whole library for a small piece of it. This lesson writes no size, because size is not in the model; tools that dump and inspect the contents of a real bundle do this job.

Startup, on the other hand, is a different measure from size. Two separate questions are asked: how many modules are loaded at launch, and when the first tree is mounted. A small bundle can contain many modules, and if all of them are run at launch, startup is still long. If most of the modules of a large bundle are deferred, startup can stay short. The `loaded.count` in the model corresponds to this second question: the number of loaded modules.

#### The Bounding Measurement

In the last measurement, the screen is loaded, shown, and then hidden. Two updates are counted: from the placeholder to the reports screen, and from the reports screen back to the placeholder. The number of loaded modules is also read after the updates. The rule is filtered to the reports screen's path.

```javascript
// main.mjs
import { reconcile, printOps } from "./fate.mjs";
import { renderApp } from "./app.mjs";
import { loadScreen, loaded } from "./loader.mjs";

const empty = renderApp({ reports: null });
const mod = await loadScreen("Reports");
const shown = renderApp({ reports: mod });
const ops = reconcile(empty, shown);
reconcile(shown, empty, "", ops);
printOps(ops.filter((op) => op.path === "/App/Reports"));
console.log(`loaded modules: ${loaded.count}`);
```

```text
mounted: /App/Reports
unmounted: /App/Reports
loaded modules: 1
```

The focus node is `/App/Reports`. In the first update, the output is mounted: this is the lesson's fate. In the second update, when the screen was hidden, the same node came out unmounted. The module count stayed at one: when the node was unmounted, the module load was not undone.

This shows the limit of deferral. Mounting and unmounting the node is the tree's job; the module staying loaded is the loader's job. The two are not tied to each other. In the model, hiding the screen does not undo the module load; the module stays loaded and is not loaded again at the second opening. At the second opening, the rule again counts the node as mounted, because the node was not in the tree. What the rule cannot see is that the second mounting does not need to load the module again.

**PP3.** The model builds this fate's rule; the model is not the framework itself. In the model, module loading is deferred with a `Promise`, and a counter counts the loaded modules. In a real application, what is deferred is either a module that sits inside the bundle with its running deferred, or a piece loaded separately from the bundle; which one it is changes with the toolchain, and the model does not tell the two apart. The bundle size, the launch time, and the loading wait time are not in the model.

#### Summary

- This lesson's fate: mounted. The `/App/Reports` node entered the tree when the deferred screen was loaded and came out mounted; in the same update, the placeholder came out unmounted.
- With lazy loading, launch loaded zero modules; with eager loading, it loaded two modules. In both launches, three nodes were mounted.
- Lazy loading changes the time of mounting, not the fate: "deferred" is not a fate.
- When the node was unmounted, the module stayed loaded; mounting and unmounting the node and loading the module are two separate jobs.
- The rule does not see the bundle size or the wait time; these must be measured on a real device and with the real bundle.

#### Next Step

In this lesson the code of a screen could be loaded, and the tree could be mounted accordingly. What happens if an error occurs while the code is loading or running? The next lesson, `Debugging Tools`, measures an error boundary unmounting the broken subtree and mounting the fallback.

### Debugging Tools

Source: https://academia.sh/en/courses/react-native/debugging-tools

An error boundary unmounts the broken subtree and mounts the fallback. The lesson measures that the same panel is updated when a prop changes, is unmounted when it throws, and loses its identity when unmounted.

The previous lesson measured when deferring the code of a screen mounts its node. The code was loaded and the tree was mounted without a problem. In a real application, not every load and not every render ends without a problem: a piece of data does not arrive in the expected form, a function receives an unexpected value, and part of the screen throws an error while being rendered. This lesson measures what that error unmounts and what it mounts in the view tree.

The course's question: **what does this update do to the node in the view tree?** The answers are kept, updated, mounted, and unmounted; the sum of everything other than kept is the commands. The rule is in the core file.

```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 mechanism that catches the error is an error boundary. An error boundary wraps the function that renders a subtree: if the function throws, the boundary catches the error, produces a fallback in place of the subtree, and the rest of the screen keeps rendering. Without a boundary, the error would bring down the whole screen. The file below builds a panel in the model, the boundary that wraps it, and the screen. The panel throws if `state.broken` is true. The boundary writes the error in a single line in plain words and returns the fallback.

```javascript
// screen.mjs — turns a screen into a view tree and wraps the broken panel in an error boundary
import { el } from "./fate.mjs";

export function renderPanel(state) {
  if (state.broken) throw new Error("data could not be read");
  return el("Panel", { total: state.total }, [el("Value", { total: state.total })]);
}

export function withBoundary(render) {
  return (state) => {
    try {
      return render(state);
    } catch (err) {
      console.log(`caught at the error boundary: ${err.message}`);
      return el("Fallback", { text: "Something went wrong" });
    }
  };
}

export function renderScreen(state) {
  const safe = withBoundary(renderPanel);
  return el("Screen", {}, [el("Header", { title: "Overview" }), safe(state)]);
}
```

`renderPanel` produces a panel tree from the state; the panel also has a value node. `withBoundary` wraps a render function: if the function throws, it catches the error, writes the message, and returns the `Fallback` node. `renderScreen` wraps the panel with the boundary and places it next to the header.

#### Healthy Update

In the first measurement, the panel does not throw; only the total changes. The header stays the same, and the panel and the value node carry the new total.

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

const before = renderScreen({ broken: false, total: 10 });
const after = renderScreen({ broken: false, total: 12 });
printOps(reconcile(before, after));
```

```text
kept: /Screen
kept: /Screen/Header
updated: /Screen/Panel
updated: /Screen/Panel/Value
```

The panel and the value came out updated, and two commands went out. The header and the root stayed kept. An updated node is the same node: its type and key have not changed, only its prop has. If there is any, its state, focus, and scroll position on the native side carry on.

#### Faulty Update

In the second measurement, the same update is made with the panel throwing. The panel throws, the boundary catches the error, and `Fallback` is returned. In the new tree, there is a fallback in the place of the panel.

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

const before = renderScreen({ broken: false, total: 10 });
const after = renderScreen({ broken: true, total: 12 });
printOps(reconcile(before, after));
```

```text
caught at the error boundary: data could not be read
kept: /Screen
kept: /Screen/Header
unmounted: /Screen/Panel
unmounted: /Screen/Panel/Value
mounted: /Screen/Fallback
```

The first line is the only line the error boundary writes. The header and the root came out kept: the error did not pass to the rest of the screen. The panel and its value became unmounted, and `Fallback` became mounted. Three commands went out: two unmounts and one mount. Because the panel's prop did not change, this unmounting does not come from a prop comparison; the type in the content area changed, `Fallback` came in place of `Panel`, and the rule unmounted the whole old subtree and mounted the whole new one.

In M11/K05, the question "what does this operation leave in the source" asks about the state of the source object after an operation: unchanged, changed, empty, or unspecified. The fate here classifies not the state of an object but whether a node's identity survives the update. **Updated keeps its identity, unmounted loses it.** In the healthy update, the panel stayed in place and the state inside it carried on. In the faulty update, the panel disappeared; the panel's native state, its focus, and its scroll position went away with it.

#### Return to Health

When the data is fixed, the tree returns to the panel. The return is not the unmounted nodes coming back; it is the rebirth of mounted nodes.

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

const broken = renderScreen({ broken: true, total: 12 });
const healed = renderScreen({ broken: false, total: 12 });
countFates(reconcile(broken, healed));
```

```text
caught at the error boundary: data could not be read
kept: 2
updated: 0
mounted: 2
unmounted: 1
commands: 3
```

The header and the root were kept. `Fallback` was unmounted, and the panel and its value were mounted. Three commands went out; the new panel is not the same as the old one but a new node. The panel's state from before the error does not come back: it disappeared when it was unmounted. The error boundary keeps the screen standing but does not carry on the life of the broken subtree.

#### The Error Boundary in the Source

A real error boundary is a component class that holds the error state. If an error occurs while rendering the child, the class switches to the error state and renders the fallback instead of the child. The model represents this with a `try` and `catch`. The snippet below shows the source form of the class; the model does not run it.

```jsx
// Platform code: Boundary
class Boundary extends React.Component {
  state = { failed: false };

  static getDerivedStateFromError() {
    return { failed: true };
  }

  render() {
    return this.state.failed ? <Fallback /> : this.props.children;
  }
}
```

The class wraps its children. `getDerivedStateFromError` changes the state when an error arrives, and the class's render function returns the fallback. On the screen, its use is a wrapping; only the wrapped subtree is under the protection of this boundary.

```jsx
// Platform code: Summary
function Summary({ total }) {
  return (
    <View>
      <Text>Overview</Text>
      <Boundary>
        <Panel total={total} />
      </Boundary>
    </View>
  );
}
```

The place of the boundary determines how much of the error it will swallow. If the boundary wraps only the panel, the error stays limited to the panel; if it wraps the whole screen, an error in one panel replaces the whole screen with the fallback. The model shows this with the place of the `withBoundary` call inside `renderScreen`: the header is outside the boundary, and this is why it stayed kept.

#### Reading the Error Boundary Snippet Line by Line

In the first snippet, four lines do the work. `state = { failed: false }` says the boundary is initially without an error. `getDerivedStateFromError()` is called when an error occurs in the subtree and returns the new state; here it sets the state to `failed: true`. `this.state.failed ? <Fallback /> : this.props.children` chooses one of two paths: the fallback if there is an error, the wrapped children if there is not.

The `withBoundary` in the model makes the same decision with a `try` and `catch`. The difference is this: in the model, the decision is made again at every call, while in a real boundary the state is stored. Once an error has occurred, a real boundary keeps showing the fallback until it is reset. This is why a retry button carries a way to reset the boundary's state.

In the second snippet, `<Boundary>` wraps only the `<Panel />` element. `<Text>Overview</Text>` is outside the boundary. This is why the header in the model stayed kept: the error occurred inside the boundary and did not pass outside it.

#### Reading the Distinction Sentence with an Example

The question of M11/K05 and the question of this lesson look similar and can be misread, so it is useful to rebuild the distinction with an example. The panel can have two separate endings.

In the first ending, only the panel's total changes. The panel is the same node, its prop takes a new value, and the fate is updated. The state inside the panel, for example an open detail row, stays in place. The identity has survived the update.

In the second ending, the panel's render throws. The panel is no longer in the tree; a node of another type has come in its place. The fate is unmounted, and every piece of state inside, such as the open detail row, goes with it. When the same user looks at the screen again, they cannot find the panel's old state; on the return to health, they see a new panel.

In both endings, it is said that "something happened to the panel", but one keeps the identity and the other loses it. Updated keeps its identity, unmounted loses it; the fate classifies not the state of an object but the fate of this identity.

#### Common Misconceptions

The first misconception is that the error boundary solves the error. The boundary catches the error and keeps the screen standing, but the cause of the error stays where it is. If the data is broken, the panel throws again at the next update; the boundary catches it again. A boundary is a fuse, not a repair.

The second misconception is that the fallback is a backup of the unmounted node. The fallback is a new node; it does not carry the panel's state. Whatever the panel held is lost with the error. An application that wants to keep the state should hold it outside the boundary, higher up.

The third misconception is that the boundary catches every error. Errors during render are caught; an error in a touch handler or in a timer is not. So instead of looking at the absence of a boundary, one looks at where the error occurred.

#### Tools Show the Tree

The everyday tools of debugging fall into three groups. The developer menu is a door for reloading the running application, seeing the logs, and opening the inspector. The debugging inspector shows the view tree of the running application: the type, the props, and the children of each node. The debugger, on the other hand, stops the code line by line and shows the variables.

All three show the tree; none changes it. The inspector does what `printOps` does in this lesson, but on the real tree: when the panel throws, it shows `Fallback` in its place, while the error message stays in the log. To find the error, the two are looked at together. If `Fallback` is seen in the tree, an error boundary has stepped in, and the source of the error is written in the log. If there is no `Fallback` in the tree and the screen has fallen, there is no boundary, or the error occurred in a place the boundary cannot catch.

There are also places the boundary does not catch. The boundary catches errors during render. An error that occurs in a touch handler or in a timer callback does not reach the boundary, since it did not occur during render; it has to be caught separately. The model builds only the error during render; the other cases stay in the prose.

#### The Bounding Measurement

The last measurement compares two updates on the same panel. In one, the panel's prop changes; in the other, the panel throws. The rule is filtered to the panel's path. The fate of the fallback and of the value node is not shown on this page, because the question is what happens to the panel.

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

const before = renderScreen({ broken: false, total: 10 });
for (const broken of [false, true]) {
  console.log(broken ? "faulty update" : "healthy update");
  const after = renderScreen({ broken, total: 12 });
  const ops = reconcile(before, after);
  printOps(ops.filter((op) => op.path === "/Screen/Panel"));
}
```

```text
healthy update
updated: /Screen/Panel
faulty update
caught at the error boundary: data could not be read
unmounted: /Screen/Panel
```

The focus node is `/Screen/Panel`. In the healthy update, the output is updated: the panel kept its identity. In the faulty update, the output is unmounted: the panel lost its identity. The lesson's fate is the second one.

The limit of this measurement is that what comes in place of the unmounted node is a fallback. The rule counts the `Fallback` node as mounted, but it does not know what that node shows the user. The fallback may say "something went wrong", may carry a retry button, or may stay empty; for the rule, all three are the same record. This is also what is useful in debugging: the tree tells where the error stopped; what the error was is told by the log and the debugger.

**PP4.** The model builds this fate's rule; the model is not the framework itself. In the model, the error boundary is a render function wrapped with a `try` and `catch`; the error is thrown from inside the function and the boundary writes it in a single line in plain words. A real error boundary is a class that holds the error state and renders its children; the place where the error is caught, the way the error state is reset, and how the error goes to the log are outside the model. In the model, the error is rebuilt at every update; a real boundary can carry the error state to the next update and can be reset with a retry button.

#### Summary

- This lesson's fate: unmounted. The `/Screen/Panel` node was unmounted when its subtree threw; `/Screen/Fallback` was mounted in its place.
- The same panel came out updated when a prop changed and kept its identity; it came out unmounted when it threw and lost its identity.
- The error did not pass to the rest of the screen: the header and the root stayed kept.
- On the return to health, the panel was mounted as a new node; the unmounted node's state did not come back.
- Debugging tools show the tree and the error and do not change the tree; the rule does not see what the fallback shows.

#### Next Step

In this lesson, the error was caught by a boundary and its effect on the tree was measured. Catching an error and verifying a piece of work come from the same need: the expected form of the tree must be known. The next lesson, `Testing`, compares an expected list of records with the output of `reconcile` and measures that an update is verified.

### Testing

Source: https://academia.sh/en/courses/react-native/testing

A test compares the update's expected list of records with the actual list of records. The lesson measures that pressing a button updates the text, and what a test cannot see.

The previous lesson measured the trace an error leaves in the tree. Debugging is finding out afterward that something went wrong; testing is writing down in advance what should go right and checking it at every change. This lesson writes the expected result of an update as a list of records and compares it with the actual list.

The course's question: **what does this update do to the node in the view tree?** The answers are kept, updated, mounted, and unmounted; the sum of everything other than kept is the commands. The rule is in the core file.

```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 model to be tested is a counter screen: a text and a button. Pressing the button increments the counter by one, and the text shows the new number. `renderCounter(count)` builds the screen. The test helper `check` takes a name, the expected records, and the old and new trees. The expected records are the fates other than `kept`, written in the form `fate: path`; `check` converts the actual records to the same form, compares them, and writes `PASS` or `FAIL`.

```javascript
// counter.mjs — turns a counter screen into a view tree
import { el } from "./fate.mjs";

export function renderCounter(count) {
  return el("Counter", {}, [
    el("Text", { value: count }),
    el("Button", { label: "Increase" }),
  ]);
}
```

```javascript
// check.mjs — compares the expected list of records with the actual list of records
import { reconcile } from "./fate.mjs";

export function check(name, expected, before, after) {
  const actual = reconcile(before, after)
    .filter((op) => op.fate !== "kept")
    .map((op) => `${op.fate}: ${op.path}`);
  const passed = JSON.stringify(actual) === JSON.stringify(expected);
  console.log(`${passed ? "PASS" : "FAIL"}: ${name}`);
  if (!passed) {
    console.log(`expected: ${expected.join(" | ") || "empty"}`);
    console.log(`actual: ${actual.join(" | ") || "empty"}`);
  }
}
```

`renderCounter` produces a tree from the state. `check` discards the `kept` records, because the question of an update is what changed; counting what did not change makes the test brittle. For this reason, the expected list writes only what changed.

#### Pressing the Button

In the first test, the user presses the button. The counter goes from zero to one. Expected: only the text node is updated, and the button and the root stay kept.

```javascript
// main.mjs
import { renderCounter } from "./counter.mjs";
import { check } from "./check.mjs";

check("pressing the button updates the text", ["updated: /Counter/Text"], renderCounter(0), renderCounter(1));
```

```text
PASS: pressing the button updates the text
```

The test passed. The actual update gave a single record: `/Counter/Text` updated. Because the button's label did not change, the button stayed kept and did not enter the expected list.

#### Unchanged Update

In the second test, nothing changes: the tree is built again with the same counter value. The expected list is empty. A re-render is not a command; this test fixes that as a rule.

```javascript
// main.mjs
import { renderCounter } from "./counter.mjs";
import { check } from "./check.mjs";

check("same value gives no command", [], renderCounter(3), renderCounter(3));
```

```text
PASS: same value gives no command
```

An empty list is also an expectation. The test fixes that an unnecessary update sends no command to the tree; if someone later adds to the counter a prop that changes at every render, this test fails.

#### Failing Test

In the third test, the expectation is wrong: the author thought the button was updated too. The actual update changes only the text. The test fails and writes the two lists one under the other.

```javascript
// main.mjs
import { renderCounter } from "./counter.mjs";
import { check } from "./check.mjs";

const expected = ["updated: /Counter/Text", "updated: /Counter/Button"];
check("wrong expectation", expected, renderCounter(0), renderCounter(1));
```

```text
FAIL: wrong expectation
expected: updated: /Counter/Text | updated: /Counter/Button
actual: updated: /Counter/Text
```

The output of the failing test says where the error is: there is one extra record in the expected list. This is a finding in both directions. Either the expectation is wrong, or the application kept the button when it should have updated it. The test does not decide which; it only shows the difference.

#### Expectation When a Node Changes

So far only props have changed. A real screen may put a warning in place of the button when a limit is reached. In this case the test's expectation moves to another kind of fate: the button is unmounted, the warning is mounted. In the measurement below, the two trees are built directly with `el`, and the expected list carries three records.

```javascript
// main.mjs
import { el } from "./fate.mjs";
import { check } from "./check.mjs";

const before = el("Counter", {}, [el("Text", { value: 9 }), el("Button", { label: "Increase" })]);
const after = el("Counter", {}, [el("Text", { value: 10 }), el("Limit", { text: "Limit reached" })]);
const expected = ["updated: /Counter/Text", "unmounted: /Counter/Button", "mounted: /Counter/Limit"];
check("at the limit the button gives way to a warning", expected, before, after);
```

```text
PASS: at the limit the button gives way to a warning
```

The test passed, and the order of the three records matched the expected one. Two things are seen here. The first is that, even though the expected list writes only what changed, an update can have more than one fate: the text was updated, the button was unmounted, the warning was mounted. The second is that `check` compares the lists in order. If the same three records had been written in a different order, the test would have failed even though the application was correct. The order of the records is a consequence of the rule; the person writing the test learns the order from the rule and does not guess it.

This measurement is a counterexample: the earlier tests had a single record each, and could have given the impression that "a test checks one record". A test checks a list of records; the list can be as long as one record or as long as three.

#### Common Misconceptions

The first misconception is that a passing test proves the application is correct. A passing test says only that the written expectation holds. If the expectation is incomplete or wrong, the application stays wrong and the test still passes. In the third test above, the situation was reversed: the expectation was wrong, and the test blamed the expectation, not the application.

The second misconception is that writing everything makes a test sturdier. This is why `check` discards the `kept` records. A test that writes the fate of every node in the tree into the list fails the moment a new node is added to the screen, even though the added node may have nothing to do with the behavior being checked. A sturdy test writes the records that the behavior it checks leaves, and stays silent about the rest.

The third misconception is that a tree test also verifies the native side. A tree test checks what the JavaScript side leaves in the tree; it does not check what appears on the screen. This distinction becomes concrete in the lesson's bounding measurement.

#### Component Test and End-to-End Test

The tests above look at the tree: a component test builds a screen, triggers an event, and checks how the tree changed. It runs fast and needs no real phone, because there are no native views; there is only the tree. The snippet below shows the source form of such a test; the model does not run it.

```jsx
// Platform code: Counter test
test("pressing the button updates the text", () => {
  render(<Counter />);
  fireEvent.press(screen.getByText("Increase"));
  expect(screen.getByText("1")).toBeTruthy();
});
```

An end-to-end test, on the other hand, opens the real application on a real screen, really touches the button, and looks at the screen. It also sees what does not show up in the tree: the pixels drawn by the native side, the keyboard opening, a permission dialog. It is slow and brittle, so there are few of them. A screen is described in prose; the model looks at the tree, and a screen is not a tree.

#### Reading the Source Snippet Line by Line

The component test's source snippet is five lines, and each line has a counterpart in the model.

- `render(<Counter />)` builds the screen into a test tree. In the model, this corresponds to producing the first tree with `renderCounter(0)`. No real native view is mounted.
- `fireEvent.press(screen.getByText("Increase"))` first finds in the tree the button whose label is "Increase", then sends it a press event. There are no events in the model; the result of the press is represented by the call `renderCounter(1)`.
- `expect(screen.getByText("1")).toBeTruthy()` says that, after the event, a node carrying the text "1" is found in the tree. Its counterpart in the model is the `/Counter/Text` node coming out updated.

The difference between them is important. The source snippet checks the result by content: is the text "1" in the tree? The model checks by fate: was the text node updated? The query does not tell whether the node was updated in place or unmounted and mounted again; both leave "1" in the tree. A test that wants to check the fate should save the node's identity in addition to the query. The rule side of this lesson makes that distinction visible.

#### The Name of a Test

The name of a test is a short sentence and says what is being tested: "pressing the button updates the text" names a behavior, "test 1" does not. A good name lets the reader understand what broke without opening the code when the test fails. For this reason, the failing test in this lesson carries the name "wrong expectation": the name says from the start that the error is in the expectation, not in the application. The name also honestly limits what the test does not test: a test that says "updates the text" says nothing about the button's label, and another test takes that on.

#### How to Write an Expectation

There is an order to writing the expected list, and the order protects against copying the list from the application. First, what the update promises the user is written in prose: "when the button is pressed, the number goes up by one." Then it is thought through which nodes this promise should touch in the tree: the node showing the number changes, the button and the root do not. Only after that is the list written and `check` run.

Reversing the order, running `check` first and pasting the actual list that comes out as the expected one makes the test pass at once, but tests nothing. Such a test freezes the application's behavior of today; if today's behavior is wrong, it freezes the wrongness too. A failing test is therefore not harmless but valuable: the difference between the expectation and the actual result is evidence that a person thought about the two things separately.

#### Which Test to Write Where

All three measurements were at the tree level. In practice, which question is asked with which test divides as follows.

- The question of which node an event changes is at the tree level. A component test answers it quickly and stably; it can run at every change.
- Whether a node keeps its identity is also a tree question, but it does not show up with a query. The difference between updated and unmounted is caught only with a tool that measures fates, or with a test that saves the node's identity.
- What appears on the screen, whether the keyboard opens, whether a permission dialog appears are outside the tree. These are the job of the end-to-end test.

An end-to-end test is slow; a real application is opened at every touch. For this reason, it is kept to a small number of tests that cover the most important paths. Tests at the tree level make up the majority. This distribution rests on the fact that most errors can be caught in the tree and that an error in the tree is found cheaply.

#### The Bounding Measurement

The last measurement shows what a test cannot see. There is no text color in the model; a detail that is not in the tree is not in the rule either. Two screens built with the same counter value produce the same tree, whatever their colors. The measurement below compares these two screens and filters to the text node.

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

const before = renderCounter(1);
const after = renderCounter(1);
const ops = reconcile(before, after);
printOps(ops.filter((op) => op.path === "/Counter/Text"));
```

```text
kept: /Counter/Text
```

The focus node is `/Counter/Text`. The two screens are the same as trees, and the text node came out kept. A test cannot know a difference that does not enter the tree, such as the text color: the rule looks at the prop, not at what is on the pixel. A test that wants to check that the color is right must carry the color into the tree as a prop, or look at the screen with an end-to-end test.

When the counter goes to one, the same filter gives `/Counter/Text` updated (the first measurement). This is the lesson's fate: a test that tests an update looks for the thing being verified on the path of the updated node.

**PP5.** The model builds this fate's rule; the model is not the framework itself. In the model, a test is a comparison of lists of records, and it discards the `kept` records. A real component test queries a text, an accessibility label, or a role; the query finds a node of the tree, but does not say whether the node it found was updated or mounted anew. The expectations measured in the model do not correspond one to one with the queries of a real test.

#### Summary

- This lesson's fate: updated. When the button was pressed, the `/Counter/Text` node was updated; the button and the root stayed kept.
- The expected list of records writes the fates other than `kept`; `check` compares it with the actual list and gives `PASS` or `FAIL`.
- The expectation of an unchanged update is the empty list; a re-render is not a command.
- An expectation is written first in prose and then as a list of records; pasting the actual list as the expected one tests nothing.
- A component test looks at the tree; a detail that does not enter the tree, for example the text color, is seen only by an end-to-end test.

#### Next Step

Tests look at the tree but do not go down to the native layer beneath it. The next lesson, `Writing Native Modules`, measures what an update does in the tree when platform-specific code is written behind a node of the tree, and it builds a bridge to the course's last lesson, `Build and Release`.

### Writing Native Modules

Source: https://academia.sh/en/courses/react-native/writing-native-modules

A native module is the bridge through which the JavaScript side calls platform-specific code. The lesson measures that a native map view is a single node in the tree and that this node is updated when the center changes.

The previous lesson measured tests that look at the tree. There is one more layer beneath the tree: when a feature is needed that the framework does not provide ready-made, for example a map or a sensor, platform-specific code is written and opened to the JavaScript side. This lesson measures how many nodes such code is in the tree and what happens to it when a prop changes.

The course's question: **what does this update do to the node in the view tree?** The answers are kept, updated, mounted, and unmounted; the sum of everything other than kept is the commands. The rule is in the core file.

```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}`);
}
```

A native module has two parts. The first is the code on the platform side: Swift or Objective-C for iOS, Kotlin for Android. The second is the bridge on the JavaScript side: it calls a native function by name and takes the result back. The file below models the bridge and a single native module. Each call writes a line, so it is visible when the native side is entered. The screen puts the native map view into the tree with a single node named `NativeMap`.

```javascript
// bridge.mjs — model of the native module and the bridge that calls it
import { el } from "./fate.mjs";

const MODULES = {
  Geo: { label: (lat, lng) => `${lat},${lng}` },
};

export function callNative(moduleName, fnName, ...args) {
  console.log(`native call: ${moduleName}.${fnName}`);
  return MODULES[moduleName][fnName](...args);
}

export function renderScreen(center) {
  const label = callNative("Geo", "label", center.lat, center.lng);
  return el("Screen", {}, [
    el("Header", { title: "Map" }),
    el("NativeMap", { label }),
  ]);
}
```

`MODULES` stands in for the native side: the `Geo` module has a single function and produces a label from a position. `callNative` finds this function by module and function name, writes the call, and returns the result. `renderScreen` takes the label from the center and puts the map into the tree as a single node.

#### First Launch

When the screen is built for the first time, the question is what happens in the tree. There is no previous tree, so every node is mounted.

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

printOps(reconcile(null, renderScreen({ lat: 41, lng: 29 })));
```

```text
native call: Geo.label
mounted: /Screen
mounted: /Screen/Header
mounted: /Screen/NativeMap
```

The native call was made once. Three nodes were mounted in the tree; the map is one of them. Many things can be inside the map: layers, markers, scroll and zoom state. The tree sees none of them; for the rule, the map is a single node.

#### When the Center Changes

The user moves the map somewhere else. The JavaScript side gives the new center and the screen is built again. The map's type and key stay the same; only its label changes.

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

const before = renderScreen({ lat: 41, lng: 29 });
const after = renderScreen({ lat: 39, lng: 32 });
printOps(reconcile(before, after));
```

```text
native call: Geo.label
native call: Geo.label
kept: /Screen
kept: /Screen/Header
updated: /Screen/NativeMap
```

Two screens were built and two native calls were made. The map came out updated; the header and the root stayed kept. An updated node keeps its identity: the native map view was not created again, the same view received the new prop. The map's scroll and zoom state therefore continues.

#### Same Center

If the center does not change, the tree gives no command. This separates the effect of a native call on the tree from the number of native calls.

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

const before = renderScreen({ lat: 41, lng: 29 });
const after = renderScreen({ lat: 41, lng: 29 });
countFates(reconcile(before, after));
```

```text
native call: Geo.label
native call: Geo.label
kept: 3
updated: 0
mounted: 0
unmounted: 0
commands: 0
```

Two native calls were made but zero commands went out. A native call and a tree command are not the same thing: a call is going down to the native side, while a command is changing a node in the tree. A call may or may not give rise to a command.

#### When the Map Leaves the Screen

Until now the map was always in the tree. If the user changes the screen, the map leaves the tree. This is the counterexample that shows where the life of a native view ends in the tree.

```javascript
// main.mjs
import { el, reconcile, printOps } from "./fate.mjs";
import { renderScreen } from "./bridge.mjs";

const before = renderScreen({ lat: 41, lng: 29 });
const after = el("Screen", {}, [el("Header", { title: "Map" })]);
printOps(reconcile(before, after));
```

```text
native call: Geo.label
kept: /Screen
kept: /Screen/Header
unmounted: /Screen/NativeMap
```

The header and the root stayed kept; the map came out unmounted. An updated map would keep its identity, while an unmounted map loses it: the native view's scroll and zoom state goes away with it. If the user returns to the map, a new node is mounted and the map starts from the state it had when first opened. The native call was made only once, because going down to the module was not needed to build the new tree.

#### How the Native Layer Looks in the Source

The platform side of a native module is code the tree never sees. The two snippets below briefly show the iOS and Android forms of the same `Geo` module; the model does not run them.

```swift
// Platform code: GeoModule (iOS)
@objc(GeoModule)
class GeoModule: NSObject {
  @objc func label(_ lat: Double, lng: Double,
                   resolve: RCTPromiseResolveBlock,
                   reject: RCTPromiseRejectBlock) {
    resolve("\(lat),\(lng)")
  }
}
```

```kotlin
// Platform code: GeoModule (Android)
class GeoModule(context: ReactApplicationContext) :
  ReactContextBaseJavaModule(context) {
  override fun getName() = "Geo"

  @ReactMethod
  fun label(lat: Double, lng: Double, promise: Promise) {
    promise.resolve("$lat,$lng")
  }
}
```

The two snippets carry the same contract: the module has a name, the function takes two numbers, and it returns the result to the JavaScript side with a promise object. This duality between platforms is an example of the distribution question in M32/K01: part of the code is written twice. The tree does not see this duality; on both platforms the screen is built with the `NativeMap` node.

#### Reading the Source Snippets Line by Line

The two snippets write the same contract in two languages; the lines match one to one.

- `@objc(GeoModule)` and `getName() = "Geo"` determine under what name the module appears on the JavaScript side. The model shows this with the `Geo` key of the `MODULES` object. If the name is not the same on the two platforms, the JavaScript side cannot find the module.
- `label(lat, lng, ...)` is the function itself. In both snippets it takes two numbers; on the Swift side there are additionally the resolve and reject blocks, and on the Kotlin side a promise object. The `label: (lat, lng) => ...` in the model is only the common core of these.
- `resolve("\(lat),\(lng)")` and `promise.resolve("$lat,$lng")` give the result back to the JavaScript side. The model does the same job with `return`; the difference is that in reality the result returns with a promise object and asynchronously.

What the lines show is this: writing a native module comes down to keeping the contract the same on both sides. If the name, the parameter order, and the returned value diverge between the platforms, the error appears not in the tree but in the bridge.

#### Common Misconceptions

The first misconception is that a native module is a view. The `Geo` module above draws nothing on the screen; it only offers a function. A view is a separate matter: the native view that draws the map is represented in the tree by the `NativeMap` node. A module can be without a view, and a view can be a set of props without a module.

The second misconception is that every native call gives rise to a command. The Same Center measurement showed that two calls gave zero commands. A call is going down to the native side; a command is a change of a node in the tree. Reducing the number of calls does not directly reduce the number of commands.

The third misconception is that the inside of a native view shows in the tree. Even if the map draws hundreds of layers, it is a single node in the tree. Looking for a fate for each of the layers is futile: the rule classifies only the nodes in the tree.

#### Module or View

Before deciding to write native code, two questions are asked. The first: is what is needed a function or a view? Reading the position, encrypting a file, or getting a sensor's value is a function; these take no space on the screen and have no nodes in the tree. A map, a camera preview, or a video player is a view; it takes space on the screen and is represented by a node in the tree.

The second: does this feature really need native code? Writing a new module for a feature that the platform offers and the framework already wraps means maintaining two code bases on two platforms. The distribution question of M32/K01 returns here: every feature written in native code enlarges the unshared part of the code. For this reason, writing a module is preferred when there is no ready-made solution and the feature is really needed.

The answers to the two questions also determine the trace in the tree. A module without a view never touches the tree; it goes down to the native side only when it is called. A module with a view leaves a node in the tree, and that node's life is written with the fates mounted, updated, and unmounted, as this lesson's measurements showed.

#### The Limit of the Bridge

The bridge is a passage between two worlds, and every crossing has a contract. The JavaScript side can give only a name and simple values; the native side can answer only with simple values or a promise object. The objects of the two sides do not pass directly to each other. This is why the `label` function in the model returns a text, not an object.

When the contract is broken, the error often appears at run time, at the moment of the call. If `MODULES[moduleName][fnName]` is called in the model with a name that does not exist, JavaScript raises an error; in the real bridge too, if the module is not found, the call fails. What catches this error is the error boundary built in a previous lesson; but the boundary catches only errors during rendering. If the call is made from a touch handler, the error must be caught separately.

This limit also explains the trace in the tree. A value that passes through the bridge enters the tree only as a prop; in the model, `label` first returns from the native call and then becomes the prop of the `NativeMap` node. The tree sees not the call itself but the value the call leaves in the tree. If the same value arrives, the node stays kept; if a different value arrives, it is updated. Keeping the value returned from the bridge simple therefore also prevents unnecessary updates in the tree.

#### The Bounding Measurement

The last measurement shows that the inner details of the native layer receive no fate in the rule. The center changes; the rule is filtered only to the path `/Screen/NativeMap`. There are no nodes in the tree for the layers and markers inside the map, so there is no record for them either.

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

const before = renderScreen({ lat: 41, lng: 29 });
const after = renderScreen({ lat: 39, lng: 32 });
const ops = reconcile(before, after);
printOps(ops.filter((op) => op.path === "/Screen/NativeMap"));
console.log(`commands: ${ops.filter((op) => op.fate !== "kept").length}`);
```

```text
native call: Geo.label
native call: Geo.label
updated: /Screen/NativeMap
commands: 1
```

The focus node is `/Screen/NativeMap`. It came out updated and a single command went out. How many layers were drawn inside the map or how many markers were moved does not enter this number: the native side does that work, and the tree knows only that the map node's prop changed.

One consequence of this limit is the following: the tree does not show an error in the native layer. Even if the map shows a wrong place, as long as its prop arrives correctly, nothing looks wrong in the tree. An error in the native layer is looked for with native tools.

The result of this is: if a native view is expensive, the cost does not show in the number of commands in the tree. The rule says that the map node's prop changed; it does not say what the native side does with that change. The count in the tree and the cost of the native work are measured separately.

**PP6.** The model builds this fate's rule; the model is not the framework itself. In the model, a native module is a function inside an object and a call is a synchronous function call. A real native module can run on a separate thread, returns with a promise, and requires the platform code to be compiled; how long the call takes and on which thread it runs are outside the model. In the model, the map is represented by a single label; the prop set of a real native view is much wider, and the change of each prop carries its own rule.

#### Summary

- This lesson's fate: updated. When the center changed, the `/Screen/NativeMap` node was updated and a single command went out.
- A native view is a single node in the tree; the layers, markers, and state inside it receive no fate for the rule.
- A native call is not a command: two calls can give zero commands.
- The same module is written separately for iOS and Android; in the tree both appear with the same node.

#### Next Step

Once native code is written, the application is no longer only JavaScript: the platform side is compiled, signed, and goes to the app store. The next lesson, `Build and Release`, is the course's wrap-up and covers this road from start to finish.

### Build and Release

Source: https://academia.sh/en/courses/react-native/build-and-release

In development, fast refresh updates the node in place, while a full reload unmounts the root. The lesson measures this difference and gathers the fates of twenty lessons in a single table.

The previous lesson measured that a native module is a single node in the tree. Once native code is written, the application stops being only JavaScript: the platform side is compiled, signed, and goes to the app store. This lesson is the course's wrap-up. First it looks at what happens in the tree when a component file changes during development, then it briefly separates the release pipeline, and at the end it gathers the fates of twenty lessons in a single table.

The course's question: **what does this update do to the node in the view tree?** The answers are kept, updated, mounted, and unmounted; the sum of everything other than kept is the commands. The rule is in the core file.

```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 model of the development session is in a separate file. The screen's source is the `source` object: the type of the root node and the title text. `buildTree` builds the tree from the source. The counter is the session's state and enters the tree as a prop of the `Counter` node. There are two ways of loading. `fastRefresh` changes the source but keeps the root key. `fullReload` increments the root key by one and resets the counter.

```javascript
// dev.mjs — model of the development session
import { el, reconcile } from "./fate.mjs";

export function buildTree(source, count, rootKey) {
  return el(source.root, {}, [
    el("Header", { title: source.title }),
    el("Counter", { count }),
  ], rootKey);
}

export function openSession(source) {
  return { source, count: 0, rootKey: null, tree: buildTree(source, 0, null) };
}

export function press(session) {
  const count = session.count + 1;
  return { ...session, count, tree: buildTree(session.source, count, session.rootKey) };
}

export function fastRefresh(session, source) {
  const draft = buildTree(source, session.count, session.rootKey);
  const ops = reconcile(session.tree, draft);
  const lost = ops.some((op) => op.fate === "unmounted" && op.path.endsWith("/Counter"));
  const count = lost ? 0 : session.count;
  const tree = lost ? buildTree(source, 0, session.rootKey) : draft;
  return [{ ...session, source, count, tree }, ops];
}

export function fullReload(session, source) {
  const rootKey = (session.rootKey ?? 0) + 1;
  const tree = buildTree(source, 0, rootKey);
  return [{ source, count: 0, rootKey, tree }, reconcile(session.tree, tree)];
}
```

The model's only rule is that state is tied to identity: the counter lives only if the `Counter` node got past the update and kept its identity. `fastRefresh` tests this with `lost`. Instead of storing or copying the state separately, it looks at the tree's comparison result.

#### First Launch

When the session is first opened, there is no previous tree. Every node is mounted and the counter is zero.

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

const session = openSession({ root: "App", title: "Home screen" });
printOps(reconcile(null, session.tree));
console.log(`counter: ${session.count}`);
```

```text
mounted: /App
mounted: /App/Header
mounted: /App/Counter
counter: 0
```

#### Fast Refresh

The user presses the counter three times, then the developer changes the title and saves the file. Fast refresh reloads only the changed component file. The root type and key stay the same.

```javascript
// main.mjs
import { printOps } from "./fate.mjs";
import { openSession, press, fastRefresh } from "./dev.mjs";

let session = openSession({ root: "App", title: "Home screen" });
session = press(press(press(session)));
const [after, ops] = fastRefresh(session, { root: "App", title: "Start" });
printOps(ops);
console.log(`counter: ${after.count}`);
```

```text
kept: /App
updated: /App/Header
kept: /App/Counter
counter: 3
```

The title came out updated; the root and the counter stayed kept. A single command went out and the counter stopped at three: the refresh did not touch the identity of the nodes, so the state lived. A save that did not change the title at all would leave all three nodes kept. Fast refresh is the most frequent pair of fates in development: the changed node is updated, the rest are kept.

#### Full Reload

Not every change fits fast refresh. If a file imported from outside the render tree changes, for example the application's startup code, the whole bundle is loaded from scratch; in the model the trace of this is the root key increasing. Adding a native module goes even beyond this: besides the bundle, it requires the platform side to be rebuilt.

```javascript
// main.mjs
import { countFates } from "./fate.mjs";
import { openSession, press, fullReload } from "./dev.mjs";

let session = openSession({ root: "App", title: "Home screen" });
session = press(press(press(session)));
const [after, ops] = fullReload(session, { root: "App", title: "Home screen" });
countFates(ops);
console.log(`counter: ${after.count}`);
```

```text
kept: 0
updated: 0
mounted: 3
unmounted: 3
commands: 6
counter: 0
```

Although the source did not change, the result is the opposite of fast refresh: three nodes unmounted, three nodes mounted, none kept. Six commands, the counter zero. When the root key changes, the comparison does not go below the root: the root is a new node, the old one disappears, and every node under it goes with it.

#### Fast Refresh Can Break Too

Fast refresh is not a privilege but a condition. If the root type changes, the comparison cannot match the root. If the developer changes the name of the root component, fast refresh also unmounts the whole tree.

```javascript
// main.mjs
import { countFates } from "./fate.mjs";
import { openSession, press, fastRefresh } from "./dev.mjs";

let session = openSession({ root: "App", title: "Home screen" });
session = press(press(press(session)));
const [after, ops] = fastRefresh(session, { root: "Shell", title: "Home screen" });
countFates(ops);
console.log(`counter: ${after.count}`);
```

```text
kept: 0
updated: 0
mounted: 3
unmounted: 3
commands: 6
counter: 0
```

The output is the same as the full reload, but the path is different: here it is not the root key but the root type that changed. The two paths arrive at the same fate. Whether the state lives depends not on which command was used but on whether the root node's identity was kept.

#### The Release Pipeline

In development, the bundle comes from the development server. In release, the bundle is embedded in the application and goes through two separate paths on two platforms. On each platform the build has three steps: compiling, signing, distribution. Compiling brings the JavaScript bundle and the native code together. Signing proves who produced the package; both platforms have their own key management. Distribution puts the signed package into a release channel: an internal test, a limited test, or a public release.

```kotlin
// Platform code: release configuration (Android)
buildTypes {
  release {
    isMinifyEnabled = true
    signingConfig = signingConfigs.getByName("release")
  }
}
```

The snippet shows two distinctions. The `release` block says that the release build is a separate configuration from the development build, and the `signingConfig` line says that the signature is a part of the build step. On the iOS side the same job is done in the project settings; the model does not separate this, because the tree does not see which key signed it.

There is one more channel: the over-the-air update. The JavaScript bundle can be changed independently of the app store release. The limit is this: only the JavaScript side changes remotely. A new version of a native module, a new permission, or a new native view requires a new store release. For this reason the `NativeMap` node of the previous lesson can be rewritten with an over-the-air update, but the map's native code cannot.

#### Common Misconceptions

The first misconception is that fast refresh always keeps the state. The root type measurement above showed the opposite: when the root type changed, the state was lost. Fast refresh is not a guarantee but a result that depends on the tree's comparison result.

The second misconception is that a full reload is restarting the application. The model does not restart the application, it builds the root again. Whether the phone's process stays up or not is outside the tree rule; the rule only says what happens to the nodes in the tree.

The third misconception is that the release configuration is reflected in the tree. The build type, the signature, and the release channel do not change the type or the props of the nodes. The same source produces the same tree in both the development and the release bundle; what differs is where the bundle that reaches the tree comes from.

#### The Bounding Measurement

The distinguishing question of this lesson is the question **where does this change reach (M12/K02)**. That question asks about the propagation of a code change: in place, linked, breaking, or implicit. This course classifies not the code but the effect of a single update at run time on a single node. In fast refresh the code change propagates in place; the trace it leaves in the tree is updated at the title node and kept in the rest. The answers to the two questions are independent of each other.

The last measurement shows the old root. The full reload is done at the root; the rule is filtered only to the records at the root level.

```javascript
// main.mjs
import { printOps } from "./fate.mjs";
import { openSession, press, fullReload } from "./dev.mjs";

let session = openSession({ root: "App", title: "Home screen" });
session = press(press(press(session)));
const [after, ops] = fullReload(session, { root: "App", title: "Home screen" });
printOps(ops.filter((op) => op.path.split("/").length === 2));
console.log(`commands: ${ops.filter((op) => op.fate !== "kept").length}`);
```

```text
unmounted: /App
mounted: /App[1]
commands: 6
```

The focus node is the old root `/App`. It came out unmounted; the new one was mounted with the key `/App[1]`. When the old root went, the title and the counter under it also received the unmounted record: six commands. An unmounted node loses its identity, that is, the counter went from three to zero. An updated root would have kept its identity.

**PP7.** The model builds this fate's rule; the model is not the framework itself. In the model, fast refresh builds the tree from the source from scratch and compares; a real fast refresh re-evaluates only the changed file and applies its own rules to keep the state. In the model the root key is incremented by hand; a real full reload loads the bundle from scratch, and its cost, and the connection of the development server, are outside the model. The release pipeline is not in the model either: the build, signing, and store processes are not inputs of the tree rule.

#### Closing Table

During the course, twenty updates were measured. Each row gives the fate of a lesson and the node focused on in that lesson's bounding measurement. The fate cells are taken from the `Summary` section of the source lesson.

| Lesson | Measured update | Fate | Bounding measurement |
|---|---|---|---|
| What Is React Native | a counter increments in a variable that does not enter the tree | kept | `/Screen/Text`, commands 0 |
| Environment Setup | the first update after the shell | mounted | `/App/Home`, with its subtree |
| Core Components | a placeholder is replaced with an image | unmounted | `/Card/Text` |
| The Style System | a style object with the same content arrives | kept | `/Row/Box`, commands 0 |
| Touch and Interaction | the pressed state becomes a prop | updated | `/Screen/Button` |
| Platform-Specific Code | the trees of the two platforms are compared | mounted | `/Settings/Toggle` |
| Navigation | a new route is pushed | kept | `/Stack/Screen[list]` |
| State Management | the cart count changes | updated | `/Screen/Header/Badge` |
| Networking and Data | data arrives | unmounted | `/Feed/Spinner` |
| Local Storage | a draft is read at launch | mounted | `/Editor/Draft` |
| Secure Storage | the session closes | unmounted | `/App/Account` |
| Notifications and Deep Linking | a link is opened | mounted | `/Stack/Screen[order-42]` |
| Gestures and Animations | the gesture continues on the native side | kept | `/Deck/Card`, commands 0 |
| List Performance | an element is added at the start | updated | `/Inbox/Row` |
| Frame Rate and Profiling | the tree is produced again three times | kept | `/Screen/Chart` |
| Bundle Size and Startup | a deferred screen is loaded | mounted | `/App/Reports` |
| Debugging Tools | a subtree raises an error | unmounted | `/Screen/Panel` |
| Testing | the button is pressed | updated | `/Counter/Text` |
| Writing Native Modules | the map center changes | updated | `/Screen/NativeMap`, commands 1 |
| Build and Release | a full reload | unmounted | `/App`, commands 6 |

The distribution of the lessons: kept 5 (What Is React Native, The Style System, Navigation, Gestures and Animations, Frame Rate and Profiling), updated 5 (Touch and Interaction, State Management, List Performance, Testing, Writing Native Modules), mounted 5 (Environment Setup, Platform-Specific Code, Local Storage, Notifications and Deep Linking, Bundle Size and Startup), unmounted 5 (Core Components, Networking and Data, Secure Storage, Debugging Tools, Build and Release).

This distribution is not the same thing as the opening count of the core file. The core's opening count counts the eight nodes of a single update: kept 5, updated 1, mounted 1, unmounted 1, and commands 3. The table, on the other hand, counts the fate of each of the twenty lessons. One counts nodes, the other counts lessons.

#### Summary

- This lesson's fate: unmounted. In the full reload, the old root `/App` came out unmounted because the root key changed; the new one became mounted, and with the three nodes under it there were six commands.
- Fast refresh leaves the changed node updated and the rest kept; the state lives. When the root type or key changes, the state is lost.
- Build, signing, and release channel are not visible in the tree; the over-the-air update changes only the JavaScript side.
- The fates of the twenty lessons are distributed five by five: five lessons each for kept, updated, mounted, and unmounted.

#### Next Step

This course measured a framework that passes shared code to the platform's native views as a description. The next course takes up a framework that does not leave the screen to the platform's views but draws it with its own rendering engine.

