Deep Dive into React Hooks: Source Code Analysis
Introduction
React Hooks, introduced in React 16.8, revolutionized how we manage state and side effects in functional components. This article delves deep into the implementation details of React Hooks, exploring how they work under the hood.
Core Concepts
React Hooks implementation is built on several fundamental concepts:
- Fiber Node: Each function component corresponds to a Fiber node where hooks state is stored
- Hook List: Multiple hooks in the same component are connected through a linked list
- Dispatcher: Responsible for switching different hook implementations in different phases (mount/update)
- WorkInProgress: Tracks the current hook being processed
- Memorization: Memory optimization mechanism for performance
Deep Dive into Hook Data Structures
Each Hook internally has the following data structure:
type Hook = {
memoizedState: any, // The state value stored by the Hook
baseState: any, // Base state value
baseQueue: Update<any, any> | null, // Base update queue
queue: UpdateQueue<any, any> | null, // Update queue
next: Hook | null, // Pointer to the next Hook
};
type UpdateQueue<S, A> = {
pending: Update<S, A> | null, // Pending updates
interleaved: Update<S, A> | null, // Interleaved updates
lanes: Lanes, // Update priority
dispatch: Dispatch<A> | null, // Dispatch function
lastRenderedReducer: ((s: S, a: A) => S) | null, // Last reducer used
lastRenderedState: S | null, // Last rendered state
};
type Hook = {
memoizedState: any, // The state value stored by the Hook
baseState: any, // Base state value
baseQueue: Update<any, any> | null, // Base update queue
queue: UpdateQueue<any, any> | null, // Update queue
next: Hook | null, // Pointer to the next Hook
};
type UpdateQueue<S, A> = {
pending: Update<S, A> | null, // Pending updates
interleaved: Update<S, A> | null, // Interleaved updates
lanes: Lanes, // Update priority
dispatch: Dispatch<A> | null, // Dispatch function
lastRenderedReducer: ((s: S, a: A) => S) | null, // Last reducer used
lastRenderedState: S | null, // Last rendered state
};
Hook Initialization Process
Here’s how hooks are created when a function component first renders:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Add hook to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Add hook to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
Complete useState Implementation
Let’s examine the detailed implementation of useState:
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
// Handle initial state
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
interleaved: 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];
}
// Update process
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
// Actual state update handling
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: null,
};
// Fast path optimization: try to compute new state immediately
if (fiber === currentlyRenderingFiber) {
const eagerState = queue.lastRenderedReducer(queue.lastRenderedState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (Object.is(eagerState, queue.lastRenderedState)) {
// State hasn't changed, no need to re-render
return;
}
}
// Add update to queue
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
scheduleUpdateOnFiber(fiber, lane);
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
// Handle initial state
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, BasicStateAction<S>> = {
pending: null,
interleaved: 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];
}
// Update process
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
}
// Actual state update handling
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: null,
};
// Fast path optimization: try to compute new state immediately
if (fiber === currentlyRenderingFiber) {
const eagerState = queue.lastRenderedReducer(queue.lastRenderedState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (Object.is(eagerState, queue.lastRenderedState)) {
// State hasn't changed, no need to re-render
return;
}
}
// Add update to queue
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
scheduleUpdateOnFiber(fiber, lane);
}
Deep useEffect Implementation
The implementation of useEffect involves scheduling and cleanup of side effects:
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// Set effect flags
currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
hook.memoizedState = pushEffect(
HookHasEffect | HookPassive,
create,
undefined,
nextDeps,
);
}
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
// Dependencies haven't changed, skip this effect
hook.memoizedState = pushEffect(HookPassive, create, destroy, nextDeps);
return;
}
}
}
// Dependencies changed, need to re-run effect
currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
hook.memoizedState = pushEffect(
HookHasEffect | HookPassive,
create,
destroy,
nextDeps,
);
}
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// Set effect flags
currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
hook.memoizedState = pushEffect(
HookHasEffect | HookPassive,
create,
undefined,
nextDeps,
);
}
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
// Dependencies haven't changed, skip this effect
hook.memoizedState = pushEffect(HookPassive, create, destroy, nextDeps);
return;
}
}
}
// Dependencies changed, need to re-run effect
currentlyRenderingFiber.flags |= UpdateEffect | PassiveEffect;
hook.memoizedState = pushEffect(
HookHasEffect | HookPassive,
create,
destroy,
nextDeps,
);
}
Custom Hooks Working Mechanism
Custom Hooks are essentially compositions of primitive hooks, sharing the same Hook list:
function useCustomHook(initialValue) {
// Create multiple Hooks in the same Fiber node's Hook list
const [state, setState] = useState(initialValue);
const prevValueRef = useRef(initialValue);
const [derived, setDerived] = useState(null);
useEffect(() => {
// Effect creation will be added to the effect list
if (state !== prevValueRef.current) {
prevValueRef.current = state;
setDerived(someComputation(state));
}
}, [state]);
const customSetState = useCallback((newValue) => {
// Callback hook also gets added to the hooks list
setState(newValue);
// Possible additional logic
}, []);
return [derived, customSetState];
}
function useCustomHook(initialValue) {
// Create multiple Hooks in the same Fiber node's Hook list
const [state, setState] = useState(initialValue);
const prevValueRef = useRef(initialValue);
const [derived, setDerived] = useState(null);
useEffect(() => {
// Effect creation will be added to the effect list
if (state !== prevValueRef.current) {
prevValueRef.current = state;
setDerived(someComputation(state));
}
}, [state]);
const customSetState = useCallback((newValue) => {
// Callback hook also gets added to the hooks list
setState(newValue);
// Possible additional logic
}, []);
return [derived, customSetState];
}
Hook Scheduling and Priority
Hook updates are managed through React’s scheduling system:
function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) {
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}
// Mark root has update
markRootUpdated(root, lane);
if (root === workInProgressRoot) {
// Handle concurrent updates
if ((workInProgressRootRenderLanes & lane) === NoLanes) {
workInProgressRootUpdatedLanes |= lane;
}
}
// Request schedule update
ensureRootIsScheduled(root);
if (lane === SyncLane) {
// Synchronous updates execute immediately
performSyncWorkOnRoot(root);
}
}
function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane) {
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}
// Mark root has update
markRootUpdated(root, lane);
if (root === workInProgressRoot) {
// Handle concurrent updates
if ((workInProgressRootRenderLanes & lane) === NoLanes) {
workInProgressRootUpdatedLanes |= lane;
}
}
// Request schedule update
ensureRootIsScheduled(root);
if (lane === SyncLane) {
// Synchronous updates execute immediately
performSyncWorkOnRoot(root);
}
}
Performance Optimization Mechanisms
React Hooks include multiple layers of performance optimization:
// 1. State batch updates
function batchedUpdates<A, R>(fn: A => R): R {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
try {
return fn();
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush synchronous queue
flushSyncCallbackQueue();
}
}
}
// 2. Memoized computation results
function useMemo<T>(
create: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = create();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
// 1. State batch updates
function batchedUpdates<A, R>(fn: A => R): R {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;
try {
return fn();
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush synchronous queue
flushSyncCallbackQueue();
}
}
}
// 2. Memoized computation results
function useMemo<T>(
create: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = create();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
Error Handling Mechanisms
Hooks include comprehensive error handling:
function throwInvalidHookError() {
throw new Error(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
function checkDepsAreArrayDev(deps: mixed) {
if (deps !== undefined && deps !== null && !Array.isArray(deps)) {
console.error(
'useEffect/useMemo/useCallback requires a dependencies array. ' +
'Did you forget to pass an array?',
);
}
}
function throwInvalidHookError() {
throw new Error(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
function checkDepsAreArrayDev(deps: mixed) {
if (deps !== undefined && deps !== null && !Array.isArray(deps)) {
console.error(
'useEffect/useMemo/useCallback requires a dependencies array. ' +
'Did you forget to pass an array?',
);
}
}
Conclusion
The React Hooks implementation is a carefully designed system that achieves its goals through:
- Clever data structure design (Hook linked list)
- Excellent memory management (memoization)
- Efficient scheduling system
- Comprehensive error handling
This creates a state management solution that is both simple to use and powerful in capability. Understanding these implementation details not only helps us better use Hooks but also helps us more quickly identify and resolve issues when they arise.