Read OSS

The Fiber Data Structure — React's Internal Representation

Advanced

Prerequisites

  • Article 1: Architecture Overview (knowing where packages live and how the fork system works)
  • JavaScript bitwise operations (&, |, ~, >>>, clz32)
  • Tree data structures and linked-list traversal

The Fiber Data Structure — React's Internal Representation

Every React component you write, every <div> you render, every Suspense boundary you wrap — all of them become Fiber nodes at runtime. The Fiber is a plain JavaScript object (technically created via a constructor function for V8 shape optimization) that holds everything React needs to process a unit of work: its type, its props, its state, its position in the tree, and what effects need to be committed to the host.

Understanding the Fiber type is prerequisite knowledge for everything that follows in this series. The work loop traverses fibers. Hooks store state on fibers. The commit phase reads flags from fibers. If fibers are the atoms of React's runtime universe, this article is your periodic table.

The Fiber Type Definition

The complete Fiber type is defined in ReactInternalTypes.js. Let's walk through the fields in logical groups.

Identity fields — What is this fiber?

Field Type Purpose
tag WorkTag Numeric type identifier (FunctionComponent, HostComponent, etc.)
key ReactKey User-provided key for reconciliation
elementType any The raw type from the React element (preserves identity during reconciliation)
type any The resolved function/class/string — may differ from elementType for hot reload
stateNode any For HostComponent: the DOM node. For ClassComponent: the class instance. For HostRoot: the FiberRoot

Tree links — Where does this fiber sit?

Field Type Purpose
return Fiber | null Parent fiber (named "return" like a stack frame's return address)
child Fiber | null First child fiber
sibling Fiber | null Next sibling fiber
index number Position among siblings

Input/output — What data flows through this fiber?

Field Type Purpose
pendingProps any Props for the current render
memoizedProps any Props from the last completed render
memoizedState any State from the last completed render (for hooks: the head of the hook linked list)
updateQueue mixed Queue of pending state updates

Scheduling and effects — When and how should React process this fiber?

Field Type Purpose
lanes Lanes Pending work priority bitmask
childLanes Lanes Pending work in the subtree
flags Flags Side effects on this fiber
subtreeFlags Flags Aggregated side effects in the subtree
deletions Array<Fiber> | null Children scheduled for removal
alternate Fiber | null The other version of this fiber (double-buffering)

Tree Structure — Child, Sibling, Return

React doesn't store children in arrays. Instead, it uses a singly-linked list: a fiber's child points to its first child, and each child's sibling points to the next child. Every fiber's return points back to its parent.

Consider this JSX:

<App>
  <Header />
  <Main />
  <Footer />
</App>

The resulting fiber tree looks like:

graph TD
    App["App<br/>tag: FunctionComponent"]
    Header["Header<br/>tag: FunctionComponent"]
    Main["Main<br/>tag: FunctionComponent"]
    Footer["Footer<br/>tag: FunctionComponent"]

    App -->|child| Header
    Header -->|sibling| Main
    Main -->|sibling| Footer
    Header -->|return| App
    Main -->|return| App
    Footer -->|return| App

This linked-list design enables the work loop (covered in Article 3) to traverse the tree without recursion and without allocating arrays. The algorithm is: go to child, process it, then follow sibling links until exhausted, then return to the parent and continue with the parent's sibling.

The FiberNode constructor initializes these links to null:

// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;

Tip: The field is called return (not parent) because it mirrors the call stack metaphor. Processing a fiber is like calling a function; when you finish, you "return" to the caller. This naming makes the work loop's behavior intuitive once you internalize it.

Double Buffering — Current and WorkInProgress

React maintains two versions of every fiber in the tree at once. The current tree is what's currently committed to the screen. The workInProgress tree is the one being built during a render.

Each fiber has an alternate pointer to its counterpart in the other tree. The createWorkInProgress function either reuses an existing alternate or creates a fresh fiber:

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    // ... set up alternate links
  } else {
    // Reuse the existing alternate, reset its fields
    workInProgress.pendingProps = pendingProps;
    // ...
  }
}
sequenceDiagram
    participant C as Current Tree
    participant W as WorkInProgress Tree
    participant S as Screen

    Note over C,S: Initial state: current is committed
    C->>W: createWorkInProgress (clone fibers)
    Note over W: Render phase: modify WIP tree
    W->>C: commitRoot: swap current pointer
    Note over C,S: WIP becomes the new current
    C->>S: DOM mutations applied

This double-buffering technique is what enables concurrent rendering. The current tree remains stable (readable by the event system, DevTools, etc.) while the workInProgress tree is being mutated. On commit, React simply swaps which tree is "current" by updating root.current.

WorkTags — The 30 Fiber Types

Every fiber has a tag field — a numeric constant from ReactWorkTags.js that identifies what kind of component this fiber represents. The beginWork function (covered in Article 3) uses a giant switch statement on this tag to dispatch to the correct processing logic.

Here are the tags grouped by category:

Tag Value Category
FunctionComponent 0 Basic components
ClassComponent 1 Basic components
HostRoot 3 Host elements
HostPortal 4 Host elements
HostComponent 5 Host elements (e.g., <div>)
HostText 6 Host elements
HostHoistable 26 Host elements (e.g., <link>, <script>)
HostSingleton 27 Host elements (e.g., <html>, <head>, <body>)
Fragment 7 Structural
Mode 8 Structural
ContextProvider 10 Context
ContextConsumer 9 Context
SuspenseComponent 13 Boundaries
SuspenseListComponent 19 Boundaries
ActivityComponent 31 Boundaries (formerly Offscreen)
MemoComponent 14 Optimization
SimpleMemoComponent 15 Optimization (function components with default comparison)
LazyComponent 16 Optimization
ForwardRef 11 Wrappers
Profiler 12 Dev tooling
ViewTransitionComponent 30 Transitions
Throw 29 Error handling

Note that value 2 is skipped (formerly IndeterminateComponent, removed in React 19) and value 20 is also absent.

Effect Flags and Phase Masks

The flags field on each fiber is a 31-bit bitmask defined in ReactFiberFlags.js. These flags tell the commit phase what side effects need to be performed.

flowchart TD
    subgraph DF["Dynamic Flags"]
        P["Placement (0b10)<br/>Insert into DOM"]
        U["Update (0b100)<br/>Apply prop changes"]
        CD["ChildDeletion (0b10000)<br/>Remove children"]
        R["Ref (0b1000000000)<br/>Attach/detach refs"]
        PA["Passive (0b100000000000)<br/>useEffect"]
        V["Visibility (0b10000000000000)<br/>Show/hide"]
    end

    subgraph PhasM["Phase Masks"]
        BM["BeforeMutationMask<br/>Snapshot"]
        MM["MutationMask<br/>Placement | Update | ChildDeletion<br/>| Ref | Hydrating | Visibility"]
        LM["LayoutMask<br/>Update | Callback | Ref | Visibility"]
        PM["PassiveMask<br/>Passive | Visibility | ChildDeletion"]
    end

    DF --> PhasM

The phase masks are the key optimization. During the commit phase, React walks the tree multiple times (before-mutation, mutation, layout, passive). Each sub-phase has a mask that defines which flags it cares about. The subtreeFlags field aggregates flags from the entire subtree, so React can skip entire subtrees that have no relevant effects.

For example, MutationMask is:

export const MutationMask =
  Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility | FormReset;

If a fiber's subtreeFlags & MutationMask is zero, the mutation sub-phase skips that entire subtree.

There are also static flags (lines 67-88) like LayoutStatic, PassiveStatic, and ViewTransitionStatic. Unlike dynamic flags which are reset between renders, static flags persist across the fiber's lifetime. They indicate that a fiber ever uses a particular kind of effect, enabling optimizations during unmount — React doesn't need to traverse a subtree looking for passive effect cleanup if PassiveStatic was never set.

FiberRoot and Root Creation

While Fiber nodes represent components in the tree, FiberRoot is the container-level data structure that holds scheduling and lifecycle state for the entire tree.

The FiberRoot is created by FiberRootNode and contains:

  • current: The committed root fiber (a HostRoot fiber)
  • containerInfo: The DOM container element
  • pendingLanes, suspendedLanes, pingedLanes, expiredLanes: Lane tracking for scheduling
  • callbackNode, callbackPriority: Scheduler integration
  • next: Link to form a linked list of roots with pending work
  • finishedWork: The completed workInProgress tree awaiting commit

The relationship between FiberRoot and the HostRoot fiber is bidirectional:

graph LR
    FR["FiberRoot<br/>(container-level state)"]
    HR["HostRoot Fiber<br/>(tag: 3)"]
    FR -->|"current"| HR
    HR -->|"stateNode"| FR
    HR -->|"child"| App["App Fiber"]

When you call createRoot(container), the DOM renderer creates a FiberRoot and its initial HostRoot fiber via createFiberRoot. The HostRoot fiber is special — it's always the topmost fiber in the tree and serves as the "entry point" fiber for rendering.

Mode Bits

Each fiber also carries a mode field — a small bitmask defined in ReactTypeOfMode.js:

export const NoMode =            0b0000000;
export const ConcurrentMode =    0b0000001;
export const ProfileMode =       0b0000010;
export const StrictLegacyMode =  0b0001000;
export const StrictEffectsMode = 0b0010000;

Mode bits are inherited from parent to child and remain unchanged after creation. ConcurrentMode enables time-slicing; ProfileMode enables the Profiler's timing measurements; StrictEffectsMode enables double-invoking effects in development.

Tip: When debugging React internals, the most useful fields to inspect on a fiber are tag (what it is), memoizedState (its current state / hook list), flags (what effects are pending), and lanes (what priority work is scheduled).

What's Next

Now that you understand the Fiber data structure — its tree shape, its double-buffered lifecycle, its type tags, and its flag system — you're ready to watch it in motion. In the next article, we'll trace the work loop: the beating heart of React that walks the fiber tree, calling beginWork on the way down and completeWork on the way up, then commits the results to the screen.