---
title: 'Build and Release'
source: 'https://academia.sh/en/courses/react-native/build-and-release'
course: 'React Native'
language: en
updated: '2026-10-21T05:02:54+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures that fast refresh leaves the node updated or kept and that the state lives on'
    - 'Shows that in a full reload the whole of the old root is unmounted and the new one is mounted'
    - 'Explains that fast refresh also loses state when the root type changes, and that build, signing, and release channels are not visible in the tree'
    - 'Gathers the fates of twenty lessons in the wrap-up table and counts the distribution'
---

# 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.
