Open-Source Wikis

/

React

/

Features

/

Hooks

facebook/react

Hooks

A hook call is one of the most common operations a React app performs. This page walks through what actually happens when a function component calls useState(0) — from the line in user code to the eventual commit.

The dispatcher pattern

packages/react/src/ReactHooks.js is the public surface. Every hook there is a one-liner:

export function useState(initialState) {
  const dispatcher = ReactSharedInternals.H;
  return dispatcher.useState(initialState);
}

ReactSharedInternals.H is the current dispatcher. It is a mutable global on react's side, set by whichever runtime is currently executing:

  • During a normal render: packages/react-reconciler/src/ReactFiberHooks.js swaps in HooksDispatcherOnMount, HooksDispatcherOnUpdate, or HooksDispatcherOnRerender depending on the phase.
  • During SSR: packages/react-server/src/ReactFizzHooks.js installs its own dispatcher with most hooks no-op'd or returning initial values.
  • During RSC rendering: packages/react-server/src/ReactFlightHooks.js installs an even more restricted dispatcher (no useState, no useEffect, …).
  • Outside any render: a sentinel dispatcher whose useState throws "Hooks can only be called inside the body of a function component."

This is the entire mechanism by which the same import { useState } from 'react' line means very different things in different contexts.

The reconciler-side machinery

packages/react-reconciler/src/ReactFiberHooks.js — at ~4,500 lines — is the largest hook implementation. Per fiber, it maintains:

  • currentlyRenderingFiber: Fiber — the fiber being rendered.
  • currentHook: Hook | null — the hook cell read on the previous render (used for updates).
  • workInProgressHook: Hook | null — the hook cell being created/updated this render.

A Hook cell looks like:

type Hook = {
  memoizedState: any,            // the hook's state value (different per hook kind)
  baseState: any,                // for useState/useReducer: the state to apply queued updates to
  baseQueue: Update | null,      // uncommitted updates from a previous render that bailed out
  queue: UpdateQueue | null,     // the active update queue
  next: Hook | null,             // linked-list pointer
};

Each fiber has a memoizedState field that points at the head of its hook list. Hooks are matched between renders by position, which is why "always call hooks in the same order" is the cardinal rule.

A useState(0) call, in detail

sequenceDiagram
  participant User as MyComponent()
  participant ReactHooks as react/ReactHooks.useState
  participant Dispatcher as ReactSharedInternals.H
  participant Reconciler as ReactFiberHooks
  participant WorkLoop as ReactFiberWorkLoop

  User->>ReactHooks: useState(0)
  ReactHooks->>Dispatcher: H.useState(0)
  Dispatcher->>Reconciler: mountState(0) on first render OR updateState(0) on later renders
  alt mount
    Reconciler->>Reconciler: allocate Hook cell, set memoizedState=0, create UpdateQueue
    Reconciler->>Reconciler: define dispatch = setState bound to fiber+queue
    Reconciler-->>User: returns [0, dispatch]
  else update
    Reconciler->>Reconciler: read previous Hook cell, apply pending updates
    Reconciler-->>User: returns [newState, sameDispatch]
  end

When the user later calls setState(7):

sequenceDiagram
  participant User as User code
  participant Dispatch as setState bound fn
  participant Reconciler as ReactFiberHooks.dispatchSetStateInternal
  participant RootSched as ReactFiberRootScheduler
  participant Sched as scheduler

  User->>Dispatch: setState(7)
  Dispatch->>Reconciler: dispatchSetStateInternal(fiber, queue, 7)
  Reconciler->>Reconciler: requestUpdateLane(fiber) — pick a lane
  Reconciler->>Reconciler: enqueue Update on the queue
  Reconciler->>Reconciler: scheduleUpdateOnFiber(root, fiber, lane)
  Reconciler->>RootSched: ensureRootIsScheduled
  RootSched->>Sched: scheduleCallback(NormalPriority, performWorkOnRoot)
  Sched-->>Reconciler: invoke performWorkOnRoot (next tick)
  Reconciler->>User: re-renders MyComponent — new render starts

The lane chosen depends on context. A setState inside a startTransition gets a transition lane. A setState inside a discrete event handler gets SyncLane. A setState outside any context gets DefaultLane.

Effect hooks

useEffect/useLayoutEffect/useInsertionEffect are stored as a different shape — each cell's memoizedState is an Effect object with tag (which phase to run in), create (the user callback), destroy (the previous render's cleanup, if any), and deps.

During render, the effect is appended to the fiber's updateQueue (different field from the hook's update queue). During commit:

  • useInsertionEffect cleanups + creates run in commitInsertionEffects (early in the mutation phase).
  • useLayoutEffect cleanups + creates run in commitLayoutEffects (synchronously, before the browser paints).
  • useEffect cleanups + creates run in flushPassiveEffects (asynchronously, after paint).

packages/react-reconciler/src/ReactFiberCommitEffects.js contains the implementation; packages/react-reconciler/src/ReactFiberCommitWork.js is the orchestrator.

use(promise)

use(promise) is the unified primitive for awaiting promises (and reading contexts) inside a render. Implementation in packages/react-reconciler/src/ReactFiberThenable.js and ReactFiberHooks.js:

  • The first time use(p) runs and p is unresolved, the reconciler throws p. The throw is caught by the nearest Suspense boundary; the promise is registered so the reconciler can retry when it resolves.
  • The second time use(p) runs (after the resolution), it pulls the resolved value from a per-fiber ThenableState and returns it.

use(context) is just a convenience for useContext — same machinery.

useEffectEvent

Stable in 19.2. The implementation lives next to the other hooks in ReactFiberHooks.js (see mountEvent/updateEvent). The semantics:

  • The returned function identity is stable (does not change between renders).
  • Each invocation of the returned function reads the current closure — i.e., the props/state of whichever render is currently committed.

This is implemented by storing both a "stable ref" (the wrapper function) and a "latest" slot that the next commit phase atomically swaps. The compiler also knows about useEffectEvent via the hook signature in compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts — it freezes the input but does not mark it as called during render, which avoids false positives in the rules-of-react validations.

Adding a new hook

The "happy path" of adding useThing:

  1. Type and export it in packages/react/src/ReactHooks.js. Add to the right export lists in ReactClient.js / ReactServer.js.
  2. Implement it in all six dispatchers in packages/react-reconciler/src/ReactFiberHooks.js: mountThing, updateThing, rerenderThing, plus their dev-mode siblings. Wire them into the dispatcher tables.
  3. Implement it in packages/react-server/src/ReactFizzHooks.js. SSR can usually return a sane initial value; the user's effect won't fire on the server anyway.
  4. Implement it in packages/react-server/src/ReactFlightHooks.js if the hook should be allowed in RSC, otherwise let it fall through to the throwing default.
  5. If the compiler should know about it, add a hook signature in compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts.
  6. Type the hook in packages/react-reconciler/src/ReactInternalTypes.js (the HookType union).
  7. Add tests. Hooks tests are typically in packages/react-reconciler/src/__tests__/ReactHooks-test.js for reconciler-side behavior and packages/react-dom/src/__tests__/ for DOM-specific surface.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Hooks – React wiki | Factory