The React version of the article is based on concurrent mode v17.02.
The hooks linked list is an unavoidable topic before discussing all hooks. This article will analyze the structure and function of the hooks linked list from a debugging perspective.
Linked List Logic During Component Mounting#
First, let's try to record this piece of code.
import { useLayoutEffect, useEffect, useState } from "react";
function UseEffectAnduseLayoutEffect() {
const [text, setText] = useState('wawawa');
useEffect(() => {
console.log('useEffect create 1');
// setText('useEffect create 1');
return () => {
console.log('useEffect destroy 1');
// setText('useEffect destroy 1');
}
}, []);
useLayoutEffect(() => {
console.log('useLayoutEffect create 1');
// setText('useLayoutEffect create 1');
return () => {
console.log('useLayoutEffect destroy 1');
// setText('useLayoutEffect destroy 1');
}
}, []);
return (
<div onClick={() => { setText('hahaha') }}>{ text }</div>
)
}
export default UseEffectAnduseLayoutEffect;
From the results, we can easily notice the function renderWithHooks, which executes UseEffectAnduseLayoutEffect. The functional component being called as a function is part of the rendering process, and this function also calls the hooks in the code.
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
...
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg); // Here we only need to focus on the scenario of calling UseEffectAnduseLayoutEffect
...
return children;
}
In React, determining whether it is a mount or an update based on the existence of current is a common operation. Since we are currently recording the operations during the initial mount, ReactCurrentDispatcher.current is HooksDispatcherOnMount. The reason for distinguishing between mount/update will be introduced later.
Next, we focus on the useState and useEffect called within UseEffectAnduseLayoutEffect. We will delve into the logic related to constructing the hooks linked list.
In the example of UseEffectAnduseLayoutEffect, the first to be called is useState, and debugging it will lead us to mountState.
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
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];
}
However, in its code, we only need to focus on mountWorkInProgressHook, which is used to construct the hook object.
Each time a hooks function is called, a hook object is created, structured as follows:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
}; // The structure of the hook object is the smallest unit that constitutes the hooks linked list
if (workInProgressHook === null) {
// This is the first hook in the list
/**
* Here we can see that the memoizedState in the fiber is used to store the hooks linked list
*/
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
/**
* Subsequent calls to other hooks will directly connect to the end of the previous hook object
*/
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
In the above code, please note the pointer workInProgressHook, which reflects which hook function is currently called in the component.
After all the useState, useEffect, and useLayout in the code have been executed, we will obtain a complete hooks linked list.
At this point, we can also see that workInProgressHook indeed points to the last hook object.
Linked List Logic During Component Updates#
Let's record the trigger of the click event and obtain the following result.
At this point, ReactCurrentDispatcher.current is HooksDispatcherOnUpdate. Therefore, when entering UseEffectAnduseLayoutEffect again, the hooks called will be different from those during the mount. At this time, if we set a breakpoint on useState, we will enter updateState within updateWorkInProgressHook.
function updateWorkInProgressHook(): Hook {
let nextCurrentHook: null | Hook;
// According to the comment, the current hook list is the list that belongs to the current fiber.
// After all, during the update operation, the current tree must already exist.
// currentHook will be set to null when the function component call is completed, so it can be used to confirm whether it is the beginning of a re-render.
if (currentHook === null) {
/**
* According to the comment, the work-in-progress fiber. I've named it differently to distinguish it from the work-in-progress hook.
* currentlyRenderingFiber is a WIP fiber
* Therefore, its alternate points to its corresponding current fiber node
*/
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
// If the current node exists, point nextCurrentHook to current.memoizedState (hooks linked list)
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
// Move the pointer forward
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
The code is lengthy, and the pointers jump around. The best way to understand it is to debug it yourself. The most important information here is that during updates, a new hooks linked list is generated using the hooks linked list on the current node. As for how to update the state, it will not be covered in this article.
Summary#
- Each time a functional component is called, a hooks linked list is generated and mounted on the corresponding fiber's memoizedState.