React Native Rendering, Part 4: The Shadow Tree — React Native's DOM
In Part 3, JSI gave JavaScript something new: the ability to reach into a C++ object and change it directly. Now it's time to meet the object it reaches into.
When you write JSX, you might assume React Native keeps one tree — the one you can see in the dev tools — and that tree becomes the screen. It doesn't. There are at least three trees between your code and the pixels, and the middle one is the one most people never notice: the Shadow Tree.
This article is about that middle tree: where it comes from, what lives inside it, and why it has to exist at all.
What You'll Learn
- The three trees of React Native: React Element Tree → Shadow Tree → Host View Tree
- What a ShadowNode is, and what it holds
- Why React Native can't just let JavaScript hold native views directly — the way the web holds DOM nodes
- Why the Shadow Tree is immutable, and how structural sharing makes that cheap
- How immutability is what makes a multi-threaded, concurrent renderer possible
Prerequisites
- JSI, and the idea of a live reference across the JS/C++ boundary:
- The three threads, and the async Bridge that JSI replaces:
Starting from a Scenario
On the web, the mental model is tidy: JSX renders into DOM nodes, and those DOM nodes are the page. document.querySelector('.card') gives you the very element that is on screen. There is one tree, and you can touch it.
Carry that model into React Native and you'll go looking for its equivalent. Instead you'll find:
- React produces a tree of components and host elements (your
Views andTexts) — call it the React Element Tree. - The screen contains a tree of real
UIViews / AndroidViews — call it the Host View Tree. - And between them there is another tree, living in C++, that you never wrote and never see: the Shadow Tree.
Why does React Native need an entire extra tree that the browser doesn't? The answer is threaded — quite literally.
Core Content
1. The three trees
Your JSX
│ React reconciliation (JS thread)
▼
┌──────────────────┐
│ React Element │ ephemeral; produced by fibers; replaced each render
│ Tree (JS) │ "what React wants the UI to be"
└────────┬─────────┘
│ commit (through JSI)
▼
┌──────────────────┐
│ Shadow Tree (C++)│ immutable; the renderer's host tree; carries props,
│ │ layout metrics, and state. ← the new home of "DOM"
└────────┬─────────┘
│ mount (on the UI thread)
▼
┌──────────────────┐
│ Host View Tree │ the real UIView / View objects that draw the pixels
│ (native) │
└──────────────────┘- The React Element Tree is what React itself works with. It is temporal — a fresh description materialized through fibers on every render. It does not persist; it is a plan.
- The Host View Tree is the actual UI on screen, and it can only be touched on the platform's UI thread.
- The Shadow Tree is the renderer's own persistent model of the UI, written in C++ and shared across platforms. It sits in between: stable enough to be a source of truth, and separate enough to be manipulated off the UI thread.
If you want one sentence: the Shadow Tree is React Native's DOM — but it lives in C++, and it is immutable.
2. What lives inside a ShadowNode
Each node in the Shadow Tree is a ShadowNode. Think of it as the host-side twin of one host element (View, Text, Image, …). A ShadowNode carries:
- Identity. It belongs to a ShadowNodeFamily: a stable identity that survives across revisions. A node's contents change; its identity does not.
- Props. The type-safe properties for that component — the same props Codegen generated C++ structs for in Part 3.
- Layout metrics. The computed position and size (x, y, width, height) after Yoga runs.
- State. A slot for C++ state that can change without a full React render (used by, for example, layout animations).
- Children. Its place in the tree.
So a ShadowNode is not just "a node." It is the complete, host-side description of one piece of UI: what it is, how it is styled, where it ended up, and what state it carries.
3. Why not just hold native views directly?
The web does exactly this: JavaScript holds references to DOM nodes directly. Why doesn't React Native let JavaScript hold references to UIViews and be done with it? Three reasons — each traces back to a constraint from Part 1.
Because of threads. Native views can only be touched on the UI thread. But React (and reconciliation) runs on the JS thread. If JS held native views directly, every render would have to happen on the UI thread — which is precisely what makes the browser freeze when JS is busy (Part 2). React Native wants its rendering work off the UI thread. That requires a copy of the UI that is safe to touch elsewhere: the Shadow Tree.
Because of cross-platform. A UIView and an Android View share almost nothing. The Shadow Tree is pure C++ and platform-agnostic, so React Native can write the tricky rendering logic once and only specialize the final mounting step.
Because React's own tree is temporarily mutable. During a render, React builds and discards intermediate work. That work cannot be the committed source of truth for a renderer that has to keep showing something now, while a low-priority render is still in progress. The renderer needs its own stable, committed representation. That is the Shadow Tree.
4. Immutability and structural sharing
Here is the design decision that unlocks everything else: the Shadow Tree is immutable.
An existing ShadowNode is never edited. When something changes — a prop, a state, a child — the renderer does not mutate the node. It clones it. The new clone gets the changed data; everything else about it is shared.
But cloning a node usually means cloning its parent too (because the parent has a new child), and so on up to the root. If you cloned the whole tree on every change, that would be catastrophic. React Native doesn't. It clones only the path from the changed node up to the root, and every unchanged subtree is shared by reference between the old tree and the new one:
Before After (one node's color changed)
root root' <- cloned
├── A ├── A' <- cloned (on the path)
│ ├── B (changed) │ ├── B' <- cloned (the change)
│ └── C ────────────┐ │ └── C ═══════╗ shared, not copied
└── D │ └── D ═══════════╝ shared
... (C, D and everything under them are reused as-is)This is structural sharing — the same trick you may know from .map() returning a new array that reuses its elements, or from immutable-state libraries. Here it is load-bearing: making a new version of the UI is cheap — proportional to the change, not to the size of the tree.
5. Why immutability is the whole point
Immutability sounds like a purity concern. It is actually the mechanism that makes a concurrent, multi-threaded renderer possible. Two payoffs:
-
Thread safety for free. If a tree is never mutated in place, then multiple threads can read different revisions of it without locks or tearing. The background thread can compute layout on revision N while the UI thread is still displaying revision N−1.
-
Multiple trees in flight. Concurrent React needs to keep more than one version of the UI around at once — the "currently shown" one, and one or more "in progress" ones at different priorities. With immutable trees plus structural sharing, keeping several versions is cheap and safe. Part 2 listed "keep multiple in-progress trees" as something concurrent React needs; this is how it becomes possible.
Analogy: imagine editing a document by never overwriting it. Instead, each edit produces a new version that reuses the untouched pages of the old one. You can now hand different readers different versions, all at once, and none of them can corrupt the others. The cost is proportional to what changed; the safety is absolute.
6. Who touches the Shadow Tree, and when
The Shadow Tree is the meeting point of all three threads, which is exactly why its immutability matters:
- JS thread — during a commit, React creates and clones ShadowNodes through JSI (the mechanism from Part 3). This is where "React wants X" becomes "the renderer's tree is revision N."
- Background thread — runs Yoga over the tree to compute layout metrics.
- UI thread — reads the finished tree, diffs it against what is on screen, and mounts the minimal changes onto native views.
Each stage hands off a revision, never a mutable object. No locks, no tearing. The rest of the series is, in a sense, a tour of what each thread does with its revision — starting, next, with the layout engine itself.
Hands-On Practice
Basic Exercises
-
Count the trees. In a small app, open React DevTools (you see the React Element Tree) and your platform's view inspector (you see the Host View Tree). Then read the Shadow Tree in the React Native source (
ShadowNode.h). Three trees, one screen. -
Feel structural sharing on paper. Given a tree of 10,000 nodes, change the color of one leaf whose parent chain is 5 nodes deep. How many nodes must be cloned? How many are shared? (Answer: 6 cloned, ~9,994 shared.)
-
Spot the identity. In the source, look at
ShadowNodeFamily. Notice that it is the thing that persists whileShadowNoderevisions come and go — a node's identity outlives its contents.
Advanced Reflection
Part 1 said React Native must keep "multiple threads in sync." Using the Shadow Tree's immutability, explain why the three threads in Section 6 don't need locks to share the tree. What would have to change if the tree were mutable instead?
Summary & Keywords
- There are three trees: the React Element Tree (ephemeral, JS), the Shadow Tree (persistent, immutable, C++), and the Host View Tree (the real native views).
- The Shadow Tree is React Native's DOM, but it lives in C++ for three reasons: thread isolation (render off the UI thread), cross-platform sharing, and a stable committed tree that React's temporary fibers cannot provide.
- It is immutable: updates clone only the changed path to the root, and unchanged subtrees are structurally shared. The cost of a new revision is proportional to the change.
- Immutability is not purity — it is the mechanism behind thread-safe rendering and multiple in-flight trees, which concurrent React requires.
Keywords: Shadow Tree, ShadowNode, ShadowNodeFamily, React Element Tree, Host View Tree, immutability, structural sharing, cloning, thread safety
Further Reading and Next Article Preview
We now have the tree. It is full of nodes that describe what to draw — but they still don't know where anything goes. For that, the renderer turns to a layout engine, and it's one you already know from the web: Flexbox. Except this Flexbox has no browser behind it. Its name is Yoga.
Next up: React Native Rendering, Part 5: Yoga — Flexbox Without a Browser.
