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
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.
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 Field | Purpose |
|---|---|
| type | The function/class/host tag this fiber represents |
| stateNode | Reference to the actual instance (DOM node, class instance) |
| child / sibling / return | Linked-list pointers forming the tree |
| pendingProps / memoizedProps | New vs. last-rendered props |
| alternate | Pointer to the corresponding fiber in the other tree (see double buffering) |
| effectTag / flags | Bitmask describing work to perform during commit |
Tip
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.
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
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 :
- Elements of different types produce different trees โ React tears down the old subtree and rebuilds rather than diffing it.
- 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
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 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:
| Priority | Example Trigger |
|---|---|
| Immediate / Sync | Discrete events like onClick, legacy sync mode |
| Input Continuous | onDrag, onScroll |
| Default | Network responses, timers |
| Transition | Updates wrapped in startTransition |
| Idle | Offscreen 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
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 Method | Phase | Hook Equivalent |
|---|---|---|
| render() | Render | Function component body |
| getSnapshotBeforeUpdate | Commit (before mutation) | No direct equivalent |
| componentDidMount / componentDidUpdate | Commit (layout) | useLayoutEffect |
| componentWillUnmount | Commit (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.
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.
- 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.
- useState is internally implemented using the same reducer mechanism as useReducer.
- useMemo/useCallback store their cached value plus dependency array in the hook node, comparing dependencies with Object.is.
Danger
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
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.
- React attempts to reuse existing DOM nodes instead of discarding and rebuilding them.
- If server and client output don't match, React logs a hydration mismatch warning and patches the DOM to match the client render.
- 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
24. Performance Optimizations ๐
React exposes several primitives to help avoid unnecessary work, all of which interact directly with internals discussed above:
| API | What it Skips |
|---|---|
| React.memo | Re-rendering a component when props are shallow-equal |
| useMemo | Recomputing an expensive value |
| useCallback | Recreating a function reference |
| useTransition | Blocking 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
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
27. Best Practices โ
- Keep component render logic pure โ no side effects, no mutating external state during render.
- Provide stable, unique keys for list items โ avoid array indices when list order can change.
- Split large contexts into smaller ones to limit unnecessary re-renders of consumers.
- Use startTransition for non-urgent state updates that might otherwise block user input.
- Prefer useLayoutEffect only when you must read layout synchronously before paint; otherwise default to useEffect.
28. Common Misconceptions โ
| Misconception | Reality |
|---|---|
| "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
Answer
Question
Answer
Question
Answer
30. Summary ๐
Summary
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! ๐