Skip to content

Back to Blog Listing

How React Renders

React represents UI as fibers, reconciles current and work-in-progress trees, preserves component state with types and keys, and stores hooks by call order.

Ben Houston7 min read

This is the first article in the React Internals series, focused on fibers, reconciliation, keys, and the storage model behind useState, useEffect, and useRef.

React's public model is simple: describe the UI for a given state, and React updates the screen. That sentence hides the part that matters when you are debugging a strange remount, a broken hook, or a list that loses input state.

React does not update the DOM directly from your component function. It builds an internal tree, compares it with the previous tree, decides which work matters, then commits the resulting mutations to the host environment.

The Problem React Solves#

React lets you describe what the UI should look like for the current props and state. When either changes, your components produce a new description. React compares it with the previous description and updates the screen to match.

Without React, your application code must keep the UI synchronized by hand. It must know which nodes already exist, which values changed, which children moved, and which event handlers need replacing. React moves that bookkeeping into the reconciler.

React could replace the entire interface after every change, but DOM nodes carry identity and browser-managed state. Replacing an unchanged input can lose its focus or text selection. Replacing a scrolling panel or media element can reset its scroll position or interrupt playback. Recreating unchanged nodes also makes the browser repeat work such as parsing, layout, and painting. React instead preserves existing nodes where it can and applies the mutations needed to make the screen match the new description.

That work is not DOM-specific. React can commit to the browser DOM, native iOS and Android views, or a Three.js scene graph. The reconciler decides what changed. A renderer decides how to apply that change to a specific platform.

Virtual DOM Means Fiber Tree#

People often describe React as keeping a "Virtual DOM" in memory. That description is close enough for the first week of React, but the more useful model is the fiber tree.

Since React 16, React has represented render work as fibers. A fiber corresponds to a component instance or host element, such as a div, View, or mesh. Each fiber stores the information React needs to continue rendering that part of the tree.

A fiber includes:

  • The component or host element type
  • The current props
  • The memoized state for that component
  • Pointers to child, sibling, and return fibers
  • An alternate pointer to the previous version of the same fiber
  • Flags for work that must happen during commit, such as placement, update, or deletion

Component tree shown beside the same fibers connected by child, sibling, and return pointers

The pointer structure matters. React does not need to render the tree through one deep recursive call stack. It can walk fibers as units of work, stop between units, and later resume from the fiber it left off on.

That choice sets up the modern concurrent renderer. A recursive traversal wants to finish once it starts. A fiber traversal can give the browser time to handle input or paint before React continues.

Current and Work-In-Progress Trees#

React keeps two versions of the tree during a render.

The current tree represents what the user sees now. The work-in-progress tree represents the next version React is preparing. Fibers in those two trees point at each other through alternate.

Current and work-in-progress fiber trees linked by alternate pointers with a commit swap

This is a form of double buffering. React can build the next tree without mutating the tree already on screen. When the render finishes, React commits the changes and the work-in-progress tree becomes current.

That distinction explains one important React rule: rendering must stay pure. During render, React may start work, pause it, throw it away, and try again. Side effects belong in the commit phase or in effects, not in the component body.

How React Diffs#

React does not compute a perfect tree edit distance. That problem is too expensive for arbitrary trees. Instead, React uses a small set of heuristics that match how UI code is usually written.

React reconciliation flow for type changes, in-place updates, and keyed child matching

The rules are practical:

  1. Different element types replace the subtree. If a <div> becomes a <span>, or Chart becomes Table, React treats the old subtree as gone. State below that point resets.
  2. Matching element types update in place. React compares props and children, then records the specific host mutations needed for commit.
  3. Keys preserve identity in lists. React uses keys to match children across renders when their positions change.

Keys deserve the attention they get. Without keys, React matches list items by position. If you insert a row at the top, React can treat every later position as changed. With stable keys, React can see that the same logical item moved.

This is why array indexes make poor keys for reorderable lists. The index identifies a position, not an item. When the list order changes, state can stick to the position and appear under the wrong row.

Hooks Live on the Fiber#

Hooks look like function calls, but React stores their state on the component's fiber.

For a function component, React keeps a linked list of hook records under the fiber's memoized state. On each render, React walks that hook list in call order. useState does not look up state by variable name. It asks for the next hook slot.

Function fiber pointing to ordered hook slots with a skipped conditional hook misaligning later slots

A simplified hook record looks like this:

type Hook = {
  memoizedState: unknown;
  queue: UpdateQueue | null;
  next: Hook | null;
};

The mental model for useState is:

function useState<T>(initial: T): [T, Dispatch<T>] {
  const hook = getNextHookSlot();

  if (isMounting) {
    hook.memoizedState = initial;
    hook.queue = createUpdateQueue();
  }

  const state = applyPendingUpdates(hook.memoizedState, hook.queue);
  hook.memoizedState = state;

  return [state as T, dispatchSetState.bind(null, hook.queue)];
}

Real React handles lanes, eager state, render-phase updates, and many edge cases. The important part is the positional storage model.

That storage model explains the rules of hooks. If you call a hook inside a conditional, React sees a different hook sequence on different renders:

if (isLoggedIn) {
  const [name, setName] = useState('');
}

When isLoggedIn changes from true to false, every later hook shifts by one slot. A useEffect might read the slot that held a useRef. React cannot recover from that because the hook state has no names attached to it. The order is the identity.

useRef and useEffect#

useRef is a hook slot that stores a stable object:

const ref = { current: initialValue };

React returns the same object on later renders. Updating ref.current does not schedule a render because no update queue fires. The ref is just mutable state attached to the fiber.

useEffect also stores data in a hook slot: the effect callback, the cleanup function, and the dependency array from the previous committed render. React does not run passive effects during render. It records them, commits the host mutations, lets the browser paint, and then runs the passive effects.

The dependency array check is a hook-slot comparison. React compares the new dependencies with the previous dependencies using Object.is. If the values match, React can skip that effect for the commit.

Why This Matters#

React's everyday rules come from these internals.

Stable keys preserve fiber identity. Hook order preserves hook identity. Pure render functions let React pause, restart, or abandon work. The current/work-in-progress split lets React prepare a tree before committing it.

Once those pieces are clear, React's concurrent rendering model feels less surprising. Fibers are already units of work. The next question is how React chooses which work to run first, when to pause, and what happens when a subtree is not ready. That is the subject of React Beyond the DOM. The optimization story continues in How React Skips Work.