---
title: 'Environment Setup'
source: 'https://academia.sh/en/courses/react-native/environment-setup'
course: 'React Native'
language: en
updated: '2026-10-21T05:02:54+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains the parts of the toolchain and the difference between the managed workflow and the native-project workflow'
    - 'Orders the places of the emulator, the physical device, and the development server in the development loop'
    - 'Measures that, because there is no previous tree on first launch, the whole tree comes out mounted'
    - 'Reads the second update, in which the shell tree and the home screen receive different fates'
---

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