React Internals: A Deep Dive Under the Hood โš›๏ธ

1. Introduction

React is one of the most widely used libraries for building user interfaces, but most developers only ever interact with its public API โ€” useState, useEffect, JSX, and components. This tutorial goes beneath that surface to explore how React actually works internally: how it schedules work, builds trees, diffs changes, and commits updates to the DOM.

Understanding React internals helps you write faster, more predictable applications, debug tricky rendering bugs, and make sense of advanced features like Concurrent Rendering and Suspense. By the end of this tutorial, you'll understand the machinery that powers every render() call.

Information

This tutorial assumes you already know React's public API (components, hooks, JSX). It focuses purely on internal mechanics.

2. React Architecture ๐Ÿ—๏ธ

Modern React (16+) is built around a layered architecture that separates what to render from how and when to render it. This separation is what enables features like time-slicing and interruptible rendering.

React Architecture
Reconciler (Fiber)
Scheduler
Renderer (react-dom, react-native)
Determines what changed
Determines when work runs
Determines how to apply changes to the host environment

This split means the same reconciliation logic can target wildly different environments โ€” the DOM, native mobile views, or even canvas โ€” simply by swapping the renderer.

3. React Fiber ๐Ÿงต

Fiber is the name of React's reimplementation of its core reconciliation algorithm, introduced in React 16. It replaced the old stack reconciler, which recursively walked the component tree synchronously and couldn't be paused once started.

A Fiber is a JavaScript object representing a unit of work corresponding to a component instance. Each Fiber holds information about a component, its input props, its output, and pointers to related fibers.

Fiber FieldPurpose
typeThe function/class/host tag this fiber represents
stateNodeReference to the actual instance (DOM node, class instance)
child / sibling / returnLinked-list pointers forming the tree
pendingProps / memoizedPropsNew vs. last-rendered props
alternatePointer to the corresponding fiber in the other tree (see double buffering)
effectTag / flagsBitmask describing work to perform during commit

Tip

Fiber is not a rendering engine on its own โ€” it's a data structure and algorithm for organizing and interrupting work.

4. Fiber Tree ๐ŸŒณ

React maintains two fiber trees at any given time: the current tree (what's on screen) and the work-in-progress (WIP) tree (what's being built). This technique is called double buffering, borrowed from graphics programming, and it lets React build a new tree without disturbing the currently rendered UI.

App (root)
Header
Main
Logo
Nav
Sidebar
Content

Each fiber links to its child (first child only), its sibling (next sibling), and its return (parent). This linked-list-of-children structure โ€” rather than an array of children โ€” allows React to traverse the tree iteratively instead of recursively, which is essential for pausing and resuming work.

When a render completes, React swaps current and workInProgress via the alternate pointer โ€” an operation known as commit.

5. Render Phase โš™๏ธ

The render phase is where React figures out what changed. It is asynchronous and interruptible โ€” React can pause, abort, or restart it without visible side effects, because nothing is committed to the DOM yet.

  • React begins at the root fiber and performs a depth-first traversal.
  • For each fiber, it calls beginWork(), which runs the component function (or renders the class) and produces child elements.
  • Once a fiber has no more children to process, completeWork() runs, finalizing that fiber and its DOM node (if a host component).
  • The traversal continues to siblings, then back up to the parent, until the whole tree is processed.

Important

Because the render phase can be interrupted, any code running inside it โ€” component bodies, most hooks โ€” must be pure and free of side effects.

6. Commit Phase ๐Ÿ’พ

The commit phase is synchronous and cannot be interrupted. This is where React actually mutates the DOM based on the effects list gathered during the render phase.

7. Reconciliation ๐Ÿ”„

Reconciliation is the process of comparing the new element tree with the previous fiber tree to determine the minimal set of changes needed. React does not diff the DOM directly โ€” it diffs lightweight React elements, then translates the result into DOM operations.

Reconciliation relies on two core assumptions to stay fast, since a fully general tree-diff algorithm is :

  1. Elements of different types produce different trees โ€” React tears down the old subtree and rebuilds rather than diffing it.
  2. Developers can hint stable identity across renders using the key prop, especially in lists.

8. Diffing Algorithm ๐Ÿงฉ

When comparing two fibers, React uses type and key to decide whether to reuse, update, or replace a node.

If the element type is unchanged (e.g. <div> to <div>), React keeps the underlying DOM node and only updates changed attributes.

If the type changes (e.g. <div> to <span>), React destroys the old subtree โ€” including its state โ€” and builds a fresh one.

For arrays of children, React iterates both lists simultaneously and matches elements by key. Missing or duplicate keys cause React to fall back to index-based matching, which can lead to incorrect state reuse.

Warning

Using array index as a key in dynamic lists is a common source of subtle bugs, especially when items are reordered, inserted, or removed.

9. Virtual DOM ๐Ÿ•ธ๏ธ

The Virtual DOM is the plain JavaScript object representation produced by React.createElement() (or the JSX transform). It's a lightweight description of what the UI should look like โ€” not a live, stateful copy of the DOM.

Simplified element shape

{
  type: 'button',
  key: null,
  props: {
    className: 'btn',
    children: 'Click me'
  }
}

The Virtual DOM's value isn't raw speed (creating objects has its own cost) โ€” it's that it enables declarative programming: you describe the desired UI state, and React figures out the imperative DOM operations needed to get there.

10. Scheduler โฐ

The Scheduler package decides when React should perform units of work. It cooperates with the browser's frame budget (~16ms per frame at 60fps) to avoid blocking user interactions, using a technique often called time-slicing.

  • Work is broken into small units (per fiber) rather than one giant synchronous pass.
  • After each unit, the scheduler checks whether the frame's time budget is exhausted via shouldYield().
  • If time runs out, control returns to the browser so it can handle input, layout, and paint โ€” React resumes later.
  • Under the hood it uses a min-heap priority queue of tasks, and prefers MessageChannel over setTimeout for scheduling callbacks with minimal delay.

11. Concurrent Rendering โšก

Concurrent Rendering (opted into via createRoot) allows React to work on multiple versions of the UI at once, pausing low-priority renders to handle urgent updates like typing or clicks.

Note

"Concurrent" doesn't mean multi-threaded. React remains single-threaded โ€” concurrency here refers to interruptible, prioritized scheduling, not parallel execution.

Concurrent features built on this foundation include useTransition, useDeferredValue, and Suspense for data fetching, all of which let developers mark updates as non-urgent so React can keep the UI responsive.

12. Lanes ๐Ÿ›ฃ๏ธ

Lanes are the mechanism React uses (since replacing the older "expiration time" model) to represent and track update priority using a bitmask of 31 bits. Each bit represents a "lane" that an update can occupy.

Simplified lane constants

export const NoLane = 0b0000000000000000000000000000000;
export const SyncLane = 0b0000000000000000000000000000010;
export const InputContinuousLane = 0b0000000000000000000000000001000;
export const DefaultLane = 0b0000000000000000000000000100000;
export const TransitionLanes = 0b0000000001111111111000000000000;
export const IdleLane = 0b0100000000000000000000000000000;

Bitwise operations make it cheap to merge, compare, and check lanes. Multiple updates can be batched into the same lane, and React always processes the most urgent pending lane first.

13. Priority Levels ๐ŸŽฏ

Every update entering React is assigned a priority, roughly corresponding to how urgently the user expects to see its result:

PriorityExample Trigger
Immediate / SyncDiscrete events like onClick, legacy sync mode
Input ContinuousonDrag, onScroll
DefaultNetwork responses, timers
TransitionUpdates wrapped in startTransition
IdleOffscreen or deprioritized work

14. Batching Updates ๐Ÿ“ฆ

Batching means grouping multiple setState calls into a single re-render instead of re-rendering after every call. Since React 18, batching happens automatically everywhere โ€” inside promises, timeouts, and native event handlers โ€” not just inside React event handlers as in React 17.

Automatic batching example

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 18: only ONE re-render, even though two state
  // updates were scheduled.
}

Tip

Need to opt out of batching for a specific update? Wrap it in flushSync() from react-dom.

15. Rendering Pipeline ๐Ÿšฆ

Putting it all together, here is the end-to-end path an update takes through React:

16. Component Lifecycle Internals ๐Ÿ”ฌ

For class components, lifecycle methods map directly onto render/commit phase steps. For function components, hooks simulate lifecycle behavior using fiber memoization.

Class MethodPhaseHook Equivalent
render()RenderFunction component body
getSnapshotBeforeUpdateCommit (before mutation)No direct equivalent
componentDidMount / componentDidUpdateCommit (layout)useLayoutEffect
componentWillUnmountCommit (layout, cleanup)Effect cleanup function

17. State Update Queue ๐Ÿ“ฌ

Each fiber with state maintains an update queue โ€” a linked list of pending updates. When you call setState, React doesn't apply it immediately; it appends an update object to this queue.

Update Queue (circular linked list)
Update A (payload, priority/lane)
Update B (payload, priority/lane)
Update C (payload, priority/lane)

During the render phase, React processes the queue in order, applying each update's payload to derive the new state โ€” this is why functional updates (setCount(c => c + 1)) are safer than relying on a stale closed-over value.

18. Hook Internals ๐Ÿช

Hooks are not magic โ€” each fiber stores a linked list of hook objects in memoizedState. Every hook call during render reads the next node in that list, in the exact order it was called.

  1. This is why hooks must be called in the same order every render โ€” React has no other way to associate a hook call with its stored state.
  2. useState is internally implemented using the same reducer mechanism as useReducer.
  3. useMemo/useCallback store their cached value plus dependency array in the hook node, comparing dependencies with Object.is.

Danger

Calling hooks conditionally (inside an if block, loop, or after an early return) desynchronizes the hook list from actual calls, corrupting state โ€” this is why the Rules of Hooks exist.

19. Context Internals ๐Ÿงญ

Context works by storing its current value on the context object itself. When a <Provider> renders, React pushes the new value; consuming fibers below read from it during render.

Internally, React keeps a dependency list on each fiber that subscribes to context. When a Provider's value changes, React walks down the fiber tree looking for consuming fibers and marks them for update โ€” this is why every consumer re-renders when context value changes, regardless of whether the specific piece it uses actually changed, unless memoized carefully.

20. Event System Internals ๐ŸŽช

React doesn't attach a listener to every individual DOM node. Instead, it uses event delegation: a single listener is attached to the root container (in React 17+; previously to document), and React simulates bubbling internally by walking up the fiber tree.

  • Improves performance by avoiding thousands of individual native listeners.
  • Allows consistent behavior across browsers via a normalized event object.
  • Since React 17, delegating to the root container (instead of document) makes it easier to embed multiple React versions/roots on one page.

21. Synthetic Events ๐Ÿงช

The event object your handlers receive (e.g. onClick={e => ...}) is a SyntheticEvent โ€” a cross-browser wrapper around the native event, pooled for performance in older React versions.

Caution

In React versions prior to 17, synthetic events were pooled and nullified after the handler ran, so accessing them asynchronously required event.persist(). This pooling was removed in React 17+.

Synthetic event handler

function handleClick(e) {
  console.log(e.type);        // "click"
  console.log(e.nativeEvent); // underlying native DOM event
}

22. Hydration Internals ๐Ÿ’ง

Hydration is the process of attaching React's event listeners and internal fiber tree to server-rendered HTML, rather than re-creating DOM nodes from scratch. React walks the existing DOM tree and matches it against the expected element tree.

  1. React attempts to reuse existing DOM nodes instead of discarding and rebuilding them.
  2. If server and client output don't match, React logs a hydration mismatch warning and patches the DOM to match the client render.
  3. Concurrent React introduced selective hydration, allowing parts of the page wrapped in Suspense to hydrate independently and prioritize hydration based on user interaction.

23. Memory Management ๐Ÿง 

React's fiber architecture is designed to minimize garbage collection pressure through fiber reuse: rather than allocating brand-new fiber objects every render, React reuses the alternate fiber from the previous tree when possible.

Best Practice

Common memory leak sources in React apps include forgetting to clean up subscriptions/timers in useEffect, and holding stale references in closures captured by long-lived callbacks.

24. Performance Optimizations ๐Ÿš€

React exposes several primitives to help avoid unnecessary work, all of which interact directly with internals discussed above:

APIWhat it Skips
React.memoRe-rendering a component when props are shallow-equal
useMemoRecomputing an expensive value
useCallbackRecreating a function reference
useTransitionBlocking urgent updates with low-priority ones

25. React Compiler Overview ๐Ÿ› ๏ธ

The React Compiler (previously known as "React Forget") is a build-time tool that automatically inserts memoization, aiming to eliminate the need for manual useMemo/useCallback/React.memo calls in most cases.

It statically analyzes component code, tracks dependencies, and generates optimized output that memoizes values and skips re-renders where safe โ€” while still relying on the same underlying fiber and reconciliation model described throughout this tutorial.

Reference

The compiler enforces the Rules of React (purity, no conditional hooks, etc.) more strictly, since incorrect assumptions could otherwise produce incorrect automatic memoization.

26. Debugging React Internals ๐Ÿž

  • Use the React DevTools Profiler to inspect commit timings, render durations, and "why did this render" flame graphs.
  • Enable strict mode (<React.StrictMode>) during development to surface impure render logic โ€” it intentionally double-invokes render and certain effects.
  • Inspect the fiber tree directly via DevTools' component tree view, which exposes hooks, props, and source location.

Hint

Unexpected re-renders are most often caused by new object/array/function references being created on every render and passed as props or context values.

27. Best Practices โœ…

  1. Keep component render logic pure โ€” no side effects, no mutating external state during render.
  2. Provide stable, unique keys for list items โ€” avoid array indices when list order can change.
  3. Split large contexts into smaller ones to limit unnecessary re-renders of consumers.
  4. Use startTransition for non-urgent state updates that might otherwise block user input.
  5. Prefer useLayoutEffect only when you must read layout synchronously before paint; otherwise default to useEffect.

28. Common Misconceptions โŒ

MisconceptionReality
"Virtual DOM makes React fast."Speed comes from diffing + minimal DOM mutation strategy, not the Virtual DOM object itself, which has its own overhead.
"Concurrent Mode runs code in parallel."React is still single-threaded; concurrency refers to interruptible scheduling, not parallel execution.
"useEffect runs synchronously after render."It runs asynchronously after paint; use useLayoutEffect for synchronous timing.

29. Frequently Asked Questions โ“

Question

Does Fiber mean React uses multiple threads?

Answer

No. Fiber enables interruptible, incremental work on a single thread, not multi-threading.

Question

Why do keys matter so much in lists?

Answer

Keys let React's diffing algorithm match elements across renders by identity rather than position, preserving state correctly during reorders.

Question

Is the Virtual DOM unique to React?

Answer

No โ€” the general technique predates React and is used by other UI libraries, though implementation details differ.

30. Summary ๐Ÿ“

Summary

React's internals center on the Fiber data structure, which enables an interruptible render phase followed by a synchronous commit phase. A Scheduler assigns priority via Lanes, enabling Concurrent Rendering and automatic batching. Hooks, context, and events all build on this same fiber-based foundation, and newer tools like the React Compiler automate optimizations developers previously had to do by hand.

Understanding these internals transforms React from a "black box" into a predictable system you can reason about โ€” helping you write more performant components and debug issues with real confidence. Happy building! ๐ŸŽ‰