Skip to main content

The Fiber Tree: React's Private Map of Your UI

What you'll learn

  • React keeps an internal tree that mirrors your element tree, one internal node per component and per host element
  • What each internal node stores, and where hook state actually lives
  • Why the element tree alone can't do this job
  • Why nodes are linked child/sibling instead of stored in arrays, and why that matters later

Here is a puzzle worth sitting with. Your component is an ordinary function. Every time it renders, all of its local variables are created from scratch, that's how functions work. And yet useState hands you the same value back, render after render. Nothing inside a function can survive the function returning. So where does the state live?

The answer: not inside your function at all. React keeps a second, persistent tree alongside your element tree, and your state lives there.

Every element gets a persistent internal node

When React renders your app, it doesn't just read the element tree and touch the DOM. It builds a parallel tree of its own, one internal node for every component element and every host element, and it keeps that tree around between renders.

Jargon: "fiber". React's name for one of these internal nodes. A fiber is a plain JavaScript object that represents one component or one host element's place in the tree, holding everything React needs to remember about it. Rendering <Counter /> means a fiber for Counter exists; the function body is just code React runs to compute what that fiber's output should be.

The name is historical; the idea is not. Think of a fiber as a workspace card React fills out for each piece of your UI: what is this thing, what props does it currently have, what state has it accumulated, which real DOM node does it correspond to, and who are its relatives?

What's inside a fiber

Pseudocode model, not real source:

// Conceptually, one fiber looks something like this:
const fiber = {
// What kind of thing is this?
tag: 'FunctionComponent', // or 'HostComponent', 'HostText', ...
type: Counter, // your function, or a string like 'button'
key: null, // the key you wrote, if any

// What it looks like right now
memoizedProps: { step: 1 }, // props used in the last committed render
memoizedState: 0, // head of the hook memory list

// Connection to the real world
stateNode: null, // for host fibers: the actual DOM node

// Family links
return: parentFiber, // parent
child: firstChildFiber, // first child
sibling: nextSiblingFiber, // next sibling
};

Two fields deserve a spotlight.

memoizedState is where your hooks live. When you call useState(0), React doesn't put the value inside your function. It appends a memory cell to a list hanging off this fiber, and hands you back the cell's current value. Your function re-runs from scratch every render; the cells on the fiber persist. (Part 3 opens this box completely.)

stateNode is the fiber's handle on reality. For a host fiber like 'button', it points at the real DOM node. When the diff decides "this button's class changed", this pointer is how React finds the button to change, no searching the DOM, ever.

Why not just use the element tree?

Fair question: the element tree already describes the UI. Why build a second tree with the same shape?

Because elements are the wrong tool for remembering anything:

  1. Elements are recreated every render. Every render produces brand-new element objects. There is nowhere to store "the value from last time" on an object that won't exist next time.
  2. Elements are immutable. React freezes them in development. Even React itself treats them as read-only snapshots of intent.
  3. React needs a workspace, not a description. Between renders, React must remember: this component's hook cells, the previous description to diff against, whether this component has pending updates, whether it's being worked on right now. All of that is mutable bookkeeping, exactly what elements forbid.

So the split of responsibilities is:

  • Element tree = the new instructions. Cheap, disposable, rebuilt from scratch each render.
  • Fiber tree = the persistent workspace. Long-lived, one card per component, holding memory and bookkeeping.

The fiber IS the component "instance"

Class components have real instances, React literally calls new YourClass() and stores the object. Function components have no instance; Counter is never constructed, just called.

But function components still need something that persists, has an identity, and owns state. That something is the fiber. When people say "the state of this component instance", for a function component they mean the hook cells hanging off this component's fiber. The fiber is the instance in every sense that matters: it's born when the component first appears at a position, it lives across renders, and it dies when the component is removed.

Walkthrough: element tree vs fiber tree

Let's make it concrete with a tiny app:

import { useState } from 'react';

function Logo() {
  return <img src="/logo.svg" alt="Acme" />;
}

function Header() {
  return (
    <header>
      <Logo />
      <h1>Acme</h1>
    </header>
  );
}

function Content() {
  const [open, setOpen] = useState(false);
  return (
    <section>
      <button onClick={() => setOpen(!open)}>
        {open ? 'Hide' : 'Show'}
      </button>
      {open && <p>Secret details</p>}
    </section>
  );
}

export default function App() {
  return (
    <main>
      <Header />
      <Content />
    </main>
  );
}

What happens:

  1. React renders App, which returns elements for <main>, <Header>, and <Content>.
  2. React renders Header and Content the same way, collecting more elements, until everything is host elements (main, header, img, h1, section, button, text…).
  3. Meanwhile, for every element it processes, React creates (or reuses) a fiber: one for App, one for main, one for Header, one for header, one for Logo, one for img, one for h1, one for Content, one for section, one for button
  4. Content's fiber gets a hook cell attached holding open: false. Once the real <img> exists, the img fiber's stateNode points straight at it.
  5. The element tree from step 2 is consumed and discarded. The fiber tree from step 3 stays.

Now the user clicks the button. setOpen(true) schedules a re-render. Content's function runs again, every local variable fresh, but useState reads from the fiber's hook cell and returns true. The memory survived because it never lived in the function.

Notice the fiber's family links: child points to the first child only, and each child points to its next sibling. Children are a linked list, not an array.

That looks awkward, you can't do fiber.children[2], and it's deliberate. With child/sibling/return pointers, React can walk the entire tree with a simple loop:

Pseudocode model, not real source:

// Walking the tree without recursion:
let node = rootFiber;
while (node !== null) {
doWork(node); // e.g. call the component, diff its output
if (node.child) { node = node.child; continue; } // go deep
while (node !== rootFiber && !node.sibling) {
node = node.return; // climb until a sibling exists
}
node = node.sibling; // go sideways
}

A loop has a superpower that recursion doesn't: it can stop in the middle and resume later. Every bit of progress lives in the node variable, not on some call stack React doesn't control. This is the seed of time-slicing, React pausing mid-render to let the browser handle a click, then continuing exactly where it left off. That's Part 4's story; for now, just notice that the linked structure is what makes "render" interruptible at all.

Arrays would also make edits expensive: inserting a child at the front of an array means shifting every other child. A linked list just re-points two links.

Diagram

Rendered diagram (PNG hi-res):

rendered diagram

Rendered diagram (PNG hi-res):

rendered diagram

Common misconceptions

  • Misconception: useState stores state in a closure inside your function. Reality: the state lives in hook cells on the component's fiber; your function just reads from them each render. That's why hooks must be called unconditionally, cells are matched by call order, not by name.
  • Misconception: The fiber tree is just another name for the element tree (the "virtual DOM"). Reality: the element tree is recreated and thrown away every render; the fiber tree persists across renders and holds all the memory.
  • Misconception: Fibers exist only for DOM elements. Reality: every component gets a fiber too, that's where its state lives.
  • Misconception: Function components have no instance, so they can't hold state. Reality: the fiber is the instance for state purposes; it just isn't a JavaScript class instance.
  • Misconception: React reads your JSX on every update to figure out what changed. Reality: JSX is consumed the moment your function returns; React diffs the new element tree against the previous fiber tree.

Why it works this way

  • State must outlive a function call. Functions are memoryless; fibers are the memory.
  • Diffing needs "before". Comparing new instructions against last time's requires storing last time's result somewhere persistent, the fiber's memoizedProps and memoizedState.
  • Updates need bookkeeping. "This component has a pending state change" or "this subtree was already visited" is mutable work-in-progress data; immutable elements can't carry it.
  • Direct DOM handles beat searching. Each host fiber's stateNode means React never queries the DOM to find the node it needs to patch.
  • Linked links make the walk pausable. Child/sibling/return turns "traverse the tree" into a resumable loop, the foundation for concurrent rendering later.

Try it yourself

  1. Open React DevTools → Components tab in any React app. The tree you see, one entry per component and host element, is essentially the fiber tree. Select a component with state: the hooks panel shows exactly the cells hanging off its fiber.
  2. Add console.log('render', Math.random()) to a component with useState, then trigger a re-render. The log proves the whole function re-ran, yet the state value didn't reset. The memory is clearly not in the function.
  3. Render the same component twice as siblings (<Counter /> and <Counter />). Give each different state. Two fibers, two independent sets of hook cells, same function, two instances.

Recap

  • React builds an internal fiber tree mirroring your element tree: one fiber per component and per host element.
  • A fiber stores: what kind of thing it is, current props, hook state (memoizedState), a pointer to the real DOM node for host elements, and parent/child/sibling links.
  • Elements can't hold memory, they're recreated every render and frozen, so React keeps a persistent workspace per component.
  • For function components, the fiber is the instance; hooks are cells on it, matched by call order.
  • Child/sibling linking turns tree traversal into a pausable loop, the hook that time-slicing will hang on later.

Next

Two trees, double buffering →