React Hook
Hooks are stored as a linked list on the fiber's memoizedState field.
Source Code
types
type Update<S, A> = {|
lane: Lane,
action: A,
hasEagerState: boolean,
eagerState: S | null,
next: Update<S, A>,
|};
type UpdateQueue<S, A> = {|
pending: Update<S, A> | null,
lanes: Lanes,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
|};
type Hook = {|
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
|};
type Effect = {|
tag: HookFlags,
create: () => (() => void) | void,
destroy: (() => void) | void,
deps: Array<mixed> | null,
next: Effect,
|};
type FunctionComponentUpdateQueue = {|
lastEffect: Effect | null,
stores: Array<StoreConsistencyCheck<any>> | null,
|};
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
source code: react/packages/react-reconciler/src/ReactFiberHooks.new.js
HookFlags
HookFlags, HookEffectTags
export type HookFlags = number;
export const NoFlags = /* */ 0b0000;
// Represents whether effect should fire.
export const HasEffect = /* */ 0b0001;
// Represents the phase in which the effect (not the clean-up) fires.
export const Insertion = /* */ 0b0010;
export const Layout = /* */ 0b0100;
export const Passive = /* */ 0b1000;
source code: react/packages/react-reconciler/src/ReactHookEffectTags.js
renderWithHooks
// The current hook list is the list that belongs to the current fiber.
let currentHook: Hook | null = null;
// The work-in-progress hook list is a new list
// that will be added to the work-in progress fiber.
let workInProgressHook: Hook | null = null;
function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes
): any {
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
const isMount = current === null || current.memoizedState === null;
ReactCurrentDispatcher.current = isMount
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
const children = Component(props, secondArg);
// We can assume the previous dispatcher is always this one, since we set it
// at the beginning of the render phase and there's no re-entrance.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
return children;
}
// renderWithHooks(current, workInProgress, Component)
// updateFunctionComponent(current, workInProgress, Component)
// beginWork(current, workInProgress)
source code: react/packages/react-reconciler/src/ReactFiberHooks.new.js
renderWithHooks renders a function component. Before calling the Component(props) function, it needs to determine which Dispatcher needs to be used depending on whether the component is during mount or update. ReactCurrentDispatcher.current is determined by current === null || current.memoizedState === null.
Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
...
}
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
...
}
ReactCurrentDispatcher.current = isMount
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
Mount
Below is an example component Counter.
function Counter() {
const [num, setNum] = React.useState(6);
const [enabled, setEnabled] = React.useState(false);
return (
<div>
<p>The number is: {num}</p>
<button>{enabled ? "ON" : "OFF"}</button>
</div>
);
}
// <Counter />
function useState(initialState) {
// HooksDispatcherOnMount | HooksDispatcherOnUpdate
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
mountState
const HooksDispatcherOnMount: Dispatcher = {
...,
useState: mountState,
...
};
function mountState<S>(
initialState: (() => S) | S
): [S, Dispatch<BasicStateAction<S>>] {
// Create a hook for the component
const hook = mountWorkInProgressHook();
if (typeof initialState === "function") {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
const dispatch: Dispatch<BasicStateAction<S>> = queue.dispatch =
dispatchSetState.bind(null, currentlyRenderingFiber, queue);
return [hook.memoizedState, dispatch];
}
-
Creates a
hookobject and populateshook.memoizedStatehook.baseStatehook.queue
-
Creates an
UpdateQueuefor thehookobject and populates the queue with the dispatch function (dispatchSetState).queue.dispatch
-
Returns the
initialStateanddispatchSetState.
Why setState is stable across different renders?
During an update, queue.dispatch will be reused in updateState -> updateReducer as the dispatch function.
function updateReducer(reducer) {
// omit irrelevant code
const hook = updateWorkInProgressHook();
const queue = hook.queue;
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
mountWorkInProgressHook
// Instantiate a hook for the component and
// stores the hook in `memoizedState` field of the component's fiber.
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
mountWorkInProgressHook stores the hooks of a component as a linked list on its memoizedState field.
Update
There are a few different ways to tell React to queue a re-render:
- Function components:
useStateuseReducer
- Class components:
this.setState()this.forceUpdate()
- Other:
- Calling the ReactDOM top-level
root.render(<App />)method again. - Updates triggered from the
useSyncExternalStorehook.
- Calling the ReactDOM top-level
dispatchSetState
An update can be triggered by setState | dispatchSetState.
// the arguments fiber and queue are fixed by `.bind`
// action is the value passed by setState
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A
) {
const lane = requestUpdateLane(fiber); // if the function is invoked by click event, lane = 1
const update: Update<S, A> = {
lane,
action, // value passed by setState, either an updater or a new state value
hasEagerState: false,
eagerState: null,
next: null,
};
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
var eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
Firstly, dispatchSetState creates an update and enqueue it to the concurrentQueues.

Secondly, scheduleUpdateOnFiber triggers a re-render by scheduling a task using ensureRootIsScheduled(root, eventTime).
During an update, when evaluating the Counter() function, the dispatcher will be HooksDispatcherOnUpdate which means that the useState will call updateReducer(basicStateReducer, initialState);.
And the pending queue of the hook returned by updateWorkInProgressHook() will have the new update. We can then use the update to compute the newState and return it to the component i.e. [hook.memoizedState, dispatch]
The source code of updateReducer can be found here.
Concurrent Update Queue
import type {
UpdateQueue as HookQueue,
Update as HookUpdate,
} from "./ReactFiberHooks.new";
type ConcurrentUpdate = {
next: ConcurrentUpdate;
lane: Lane;
};
type ConcurrentQueue = {
pending: ConcurrentUpdate | null;
};
const concurrentQueues: Array<any> = [];
function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane
): FiberRoot | null {
const concurrentQueue: ConcurrentQueue = queue;
const concurrentUpdate: ConcurrentUpdate = update;
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
return getRootForUpdatedFiber(fiber); // FiberRootNode
}
function enqueueUpdate(
fiber: Fiber,
queue: ConcurrentQueue | null,
update: ConcurrentUpdate | null,
lane: Lane
) {
// Don't update the `childLanes` on the return path yet. If we already in
// the middle of rendering, wait until after it has completed.
concurrentQueues[concurrentQueuesIndex++] = fiber;
concurrentQueues[concurrentQueuesIndex++] = queue;
concurrentQueues[concurrentQueuesIndex++] = update;
concurrentQueues[concurrentQueuesIndex++] = lane;
concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);
// The fiber's `lane` field is used in some places to check if any work is
// scheduled, to perform an eager bailout, so we need to update it immediately.
// TODO: We should probably move this to the "shared" queue instead.
fiber.lanes = mergeLanes(fiber.lanes, lane);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
}
source code: react/packages/react-reconciler/src/ReactFiberConcurrentUpdates.new.js
When do we make use of concurrentQueues?
Ans: We use finishQueueingConcurrentUpdates to add the update to the hook.queue's pending list before the next render (in prepareFreshStack for HostRoot).
finishQueueingConcurrentUpdates also invokes markUpdateLaneFromFiberToRoot(fiber, update, lane) which updates the source fiber's lanes and walks the parent path to the root and update the childLanes.