Architecture facebook/react

React Hooks Dispatcher: How useState Resolves on Mount and Re-render

facebook/react MIT
How to read this page

Symvanta parsed this repository into a code graph: every function, class, and method is a node, and every call or import between them is an edge. Everything on this page is computed from that graph at the commit shown above. This page is a deep dive into one subsystem of React Architecture: How the Codebase Actually Works; the hub page holds the whole-repo module map and glossary.

When you call useState in a component, the function in the react package does almost nothing: it reads a mutable slot called the dispatcher and forwards the call to whatever the reconciler installed there a moment ago. That indirection is the entire hooks mechanism. The react package ships a hooks API with no implementation, and the reconciler swaps in one of two concrete implementations right before it runs your component. Symvanta's graph shows the two halves living in different clusters: the public hook functions sit in the React Rendering Core cluster (hub useEffect, 1,376 symbols), while the mount and update implementations, plus the machinery that installs them, live in the reconciler's React Fiber Core cluster (1,928 symbols). The single field that connects them is ReactSharedInternals.H.

This indirection is why the same useState line behaves differently on a component's first render than on every render after, why hook state can live on the fiber instead of in a closure, and why the "Invalid hook call" warning exists at all. This analysis is generated from Symvanta's code graph over facebook/react at commit f598ec1.

The moving parts

ReactSharedInternals is a module-level singleton, re-exported from React's internal client bundle, whose H field holds the currently active dispatcher (or null). resolveDispatcher is the two-line function every public hook calls first; it returns ReactSharedInternals.H. The Dispatcher interface is the contract both implementations satisfy: useState, useReducer, useEffect, useContext, useRef, and the rest, roughly two dozen slots. HooksDispatcherOnMount and HooksDispatcherOnUpdate are the two concrete objects: on mount, useState maps to mountState; on update, it maps to updateState (which delegates to updateReducer). mountWorkInProgressHook and updateWorkInProgressHook are the two functions that manage where hook state lives: a singly linked list of Hook objects hanging off the fiber's memoizedState field, one node per hook call, in call order.

How it works

Here is the canonical path a useState call travels, showing where the first render and every render after diverge.

  1. useState in the react package is five lines: const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState);. It holds no state and knows nothing about fibers. Every other hook (useEffect, useReducer, useContext) has the same shape.
  2. resolveDispatcher returns ReactSharedInternals.H. In development it first checks whether H is null and, if so, logs the "Invalid hook call. Hooks can only be called inside of the body of a function component" message. The comment in the source notes it deliberately does not throw its own error (a null access will throw naturally, and skipping the check keeps this hot path inlinable).
  3. renderWithHooks is the reconciler function that made H non-null in the first place. Before it calls your component, it resets the work-in-progress fiber's memoizedState, updateQueue, and lanes, then assigns ReactSharedInternals.H to either the mount or the update dispatcher. The choice is one condition: current !== null && current.memoizedState !== null, meaning the fiber already rendered once and used at least one stateful hook, selects the update dispatcher; otherwise the mount dispatcher.
  4. HooksDispatcherOnMount is installed on first render. Its useState slot is mountState, which calls mountWorkInProgressHook to allocate a fresh Hook, stores the initial state, and binds dispatchSetState to the fiber and the hook's queue to produce the setter you get back.
  5. mountWorkInProgressHook is where hook state attaches to the fiber. The first hook call sets currentlyRenderingFiber.memoizedState to the new node; each later call appends to the previous node's next. The result is a linked list whose order is exactly the order your hooks were called, with no keys or names.
  6. HooksDispatcherOnUpdate is installed on every render after the first. Its useState slot is updateState, which forwards to updateReducer with a basic state reducer, since a state update is just a reducer update in disguise.
  7. updateWorkInProgressHook walks that list by position on re-render. It reads the next node from the alternate fiber's memoizedState (the previous render's list) and clones it forward. If it runs off the end of the list, it throws "Rendered more hooks than during the previous render." That single positional walk is why the Rules of Hooks forbid calling hooks conditionally: the list has no names, so position is the only identity a hook has.

Where it connects

The dispatcher is a deliberately thin seam between two clusters that the module map keeps separate. The public hook functions and DevTools' hook inspection types cluster together in React Rendering Core (hub useEffect), while the dispatchers, the Hook list machinery, and renderWithHooks live in the reconciler's React Fiber Core. Neither imports the other's internals; they meet only through ReactSharedInternals.H, which is also how a react-dom build and a react-native build can install different renderers behind the identical react package the app imports.

That seam is the same one the Fiber reconciler spoke describes from the render side: renderWithHooks is called from beginWork as the reconciler descends the tree, and the memoizedState linked list every hook reads and writes hangs off the very Fiber objects the render phase walks and the commit phase applies. When dispatchSetState (the setter mountState bound in step 4) fires later, it schedules a new render, renderWithHooks runs again, and this time the update dispatcher walks the existing list instead of building it.

By the numbers

The Dispatcher interface is satisfied by three separate implementations at this commit, HooksDispatcherOnMount, HooksDispatcherOnUpdate, and a rerender variant, each mapping roughly two dozen hook names to distinct functions, plus a parallel set of development-mode dispatchers that add hook-order validation. renderWithHooks spans 130 lines (502 to 631), most of it the branching that selects which of those dispatchers to install. The whole mechanism turns on one nullable field: ReactSharedInternals.H is non-null only during the window between renderWithHooks setting it and resetting it, which is the precise definition of "inside the body of a function component" that the "Invalid hook call" warning enforces.

See your own codebase mapped like this.

Book a demo →

Auto-generated by Symvanta from the public repo facebook/react at commit f598ec1 , licensed MIT .

Get this for your codebase →