In the previous article, we discussed the building and updating process of functional components. This article will delve into how React hooks work.
First, let us understand the classification of hooks:
- State Hook: such as
useStateanduseReducer - Side effect Hook (Effect Hook): such as
useEffectanduseLayoutEffect - Ref Hook: such as
useRef
This article will talk about the implementation of state Hook, useState and useReducer.
2. Implementation of useState
useState is one of the most commonly used Hooks in React. Its implementation is divided into two main functions: mountState (for initialization) and updateState (for updates). Let us first look at the implementation of mountState.
2.1 mountState
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}Let's analyze this function line by line:
const hook = mountWorkInProgressHook();This line of code creates a new hook object and adds it to the current fiber's hooks list. This is the key mechanism for React to manage multiple hooks.if (typeof initialState === 'function') { initialState = initialState() }This is an optimization that allows the user to pass in a function to calculate the initial state. This is useful for complex or expensive initial state calculations, as it will only be performed once when the component is mounted.
hook.memoizedState = hook.baseState = initialState;The hook'smemoizedStateandbaseStateare initialized here.memoizedStatestores the current state, whilebaseStateis used to calculate the new state during the update process.const queue = (hook.queue = { pending: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), });This creates an update queue.
pendingis used to store pending updates,dispatchis the update function,lastRenderedReduceris the reducer used in the last rendering (for useState, this is a basic state update function),lastRenderedStateis the state of the last rendering.const dispatch: Dispatch< BasicStateAction<S>, > = (queue.dispatch = (dispatchAction.bind( null, currentlyRenderingFiber, queue, ): any));This creates the
dispatchfunction. It is actually a bound version of thedispatchActionfunction, which pre-binds the current fiber and update queue. This is an optimization that avoids having to rebind these parameters every timedispatchis called.return [hook.memoizedState, dispatch];Finally, the current state and dispatch function are returned. This is the array we get when usinguseState.
2.2 dispatchAction
Next, let's look at the dispatchAction function, which is the core of status updates:
function dispatchAction<S, A>(
fiber: fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber);
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
}
}
}
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
}Let's analyze this function step by step:
const eventTime = requestEventTime() const lane = requestUpdateLane(fiber)These two lines of code get the current event time and update priority (lane). React uses the lane model to manage the priority of updates, which is an important optimization mechanism.
const update: Update<S, A> = { lane, action, eagerReducer: null, eagerState: null, next: (null: any), };This creates a new update object.
lanerepresents the priority of the update,actionis the action of state update,eagerReducerandeagerStateare used for optimization, andnextis used to link to the next update.const pending = queue.pending if (pending === null) { update.next = update } else { update.next = pending.next pending.next = update } queue.pending = updateThis code adds new updates to the update queue. Note that the circular linked list structure is used here, which allows React to handle multiple updates efficiently.
if (fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) { didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true } else { // ... (optimization logic) }This code checks if an update is scheduled during the render phase. If so, it sets a flag so that React can start a new render as soon as the current render completes.
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) { // ... (optimization logic) }This is an important optimization. If there are no pending updates to the current fiber, React will try to calculate the new state immediately.
const lastRenderedReducer = queue.lastRenderedReducer if (lastRenderedReducer !== null) { // ... (optimization logic) }Here React tries to use the reducer from the last render to calculate the new state. This is an optimization that avoids unnecessary re-rendering.
const currentState: S = (queue.lastRenderedState: any); const eagerState = lastRenderedReducer(currentState, action); update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (is(eagerState, currentState)) { return; }This code calculates the new state (
eagerState). If the new state is the same as the current state, the function returns directly, avoiding unnecessary updates.scheduleUpdateOnFiber(fiber, lane, eventTime);If an update is needed, this line of code will schedule an update.
2.3 updateState
When the component is updated, updateState is called. This function actually calls updateReducer:
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}updateReducer is a complex function that handles the actual updating of state. Let's look at its key parts:
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
// ... (process baseQueue and pendingQueue)
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// The priority is not enough, skip this update
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
} else {
// Priority is sufficient, apply this update
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
lane: NoLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
if (update.eagerReducer === reducer) {
// If there is a cached calculation result, use it directly
newState = ((update.eagerState: any): S);
} else {
// Otherwise, call the reducer to calculate the new state
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
// ... (update hook status)
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}The core of this function is a loop that iterates through all pending updates. Let's analyze it step by step:
const hook = updateWorkInProgressHook();This line of code gets the currently working hook. React uses a linked list structure to manage multiple hooks in a component, which allows the hooks to maintain a stable order during rendering.queue.lastRenderedReducer = reducer;Update the reducer used by the last rendering stored in the queue. This is for subsequent optimization.The next big loop is the core of the entire function, which traverses all pending updates.
if (!isSubsetOfLanes(renderLanes, updateLane)) { ... }This condition checks whether the priority of the current update is high enough. If the priority is not high enough, the update will be skipped and added to a new queue, waiting for the next rendering.if (update.eagerReducer === reducer) { ... }This is an important optimization. If the current update already has a precomputed result (eagerState), and the reducer used has not changed, then the precomputed result will be used directly to avoid repeated calculations.newState = reducer(newState, action);If there is no precomputed result, the reducer is called to calculate the new state.
This function demonstrates several key optimizations in React's state update process:
- Priority scheduling: Through the lane model, React can skip low-priority updates and prioritize high-priority updates.
- Batch updates: Multiple updates are placed in a queue and processed together, improving efficiency.
- Precomputation: Through eagerReducer and eagerState, React can avoid double calculation in certain situations.
3. Implementation of useReducer
The implementation of useReducer is very similar to useState. The main difference is that useReducer allows users to provide custom reducer functions. Let’s look at the implementation of mountReducer:
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}This function is very similar to mountState, the main differences are:
- It accepts a
reducerfunction as a parameter. - It allows the state to be initialized through the
initfunction, which provides a more flexible initialization method.
The update process of useReducer (updateReducer) is exactly the same as useState, which we have analyzed in detail before.
4. Think deeply
Essence of Hooks: Hooks are essentially a method of state management. They allow functional components to have their own state without needing to be converted to class components. This greatly simplifies writing React components.
Linked list structure: React uses linked lists to manage hooks, which allows hooks to maintain a stable order between multiple renderings. This is why hooks cannot be used in conditional statements.
Batch update and priority: React’s update mechanism is very sophisticated. It can batch multiple updates and decide which updates should be processed first based on priority. This greatly improves the performance and responsiveness of the application.
Lazy initialization: Both
useStateanduseReducersupport lazy initialization, that is, a function can be passed in to calculate the initial state. This is useful for complex initial state calculations to avoid having to recalculate them on every render.Closure Trap: The implementation of Hooks relies on JavaScript’s closure mechanism. This can lead to subtle bugs, such as using old state in asynchronous operations. Understanding this is very important for using hooks correctly.