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.jsswaps inHooksDispatcherOnMount,HooksDispatcherOnUpdate, orHooksDispatcherOnRerenderdepending on the phase. - During SSR:
packages/react-server/src/ReactFizzHooks.jsinstalls its own dispatcher with most hooks no-op'd or returning initial values. - During RSC rendering:
packages/react-server/src/ReactFlightHooks.jsinstalls an even more restricted dispatcher (nouseState, nouseEffect, …). - Outside any render: a sentinel dispatcher whose
useStatethrows "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]
endWhen 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:
useInsertionEffectcleanups + creates run incommitInsertionEffects(early in the mutation phase).useLayoutEffectcleanups + creates run incommitLayoutEffects(synchronously, before the browser paints).useEffectcleanups + creates run influshPassiveEffects(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 andpis unresolved, the reconciler throwsp. 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-fiberThenableStateand 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:
- Type and export it in
packages/react/src/ReactHooks.js. Add to the right export lists inReactClient.js/ReactServer.js. - 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. - 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. - Implement it in
packages/react-server/src/ReactFlightHooks.jsif the hook should be allowed in RSC, otherwise let it fall through to the throwing default. - If the compiler should know about it, add a hook signature in
compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts. - Type the hook in
packages/react-reconciler/src/ReactInternalTypes.js(theHookTypeunion). - Add tests. Hooks tests are typically in
packages/react-reconciler/src/__tests__/ReactHooks-test.jsfor reconciler-side behavior andpackages/react-dom/src/__tests__/for DOM-specific surface.
Related pages
- packages/react — the public surface.
- packages/react-reconciler — the implementation.
- features/concurrent-rendering — how
setStatelanes get scheduled. - features/suspense — how
use(promise)interacts with Suspense.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.