The Fiber Data Structure — React's Internal Representation
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(notparent) 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 elementpendingLanes,suspendedLanes,pingedLanes,expiredLanes: Lane tracking for schedulingcallbackNode,callbackPriority: Scheduler integrationnext: Link to form a linked list of roots with pending workfinishedWork: 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), andlanes(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.