- React source version: v16.11.0
- Source code notes: airingursb/react
Before writing this article, I read some articles online about the source code analysis of Hooks, which were either too superficial or not detailed enough. Therefore, this article focuses on explaining the source code, going from shallow to deep, striving not to miss a single line of code. The first Hooks explained in this series is useState. We will start with the usage of useState, then explain the rules, discuss the principles, implement it simply, and finally analyze the source code. Additionally, at the beginning of this article, I will add an overview of Hooks, which has not been covered in the previous two articles due to space constraints.
Note: It has been two months since the last article, and due to busy work, there hasn't been much time to update this series. However, React has updated from 16.9 to 16.11 in these two months, and after reviewing the updates, none of them involved hooks, so I have directly updated the source code notes to 16.11.
1. Overview of React Hooks#
Hooks are a new feature introduced in React 16.8 that allows you to use state and other React features without writing a class. Essentially, they are a special type of function that starts with use
, allowing you to inject some functionality into Function Components, giving Function Components some capabilities that Class Components have.
For example, we used to say that Function Components cannot maintain state, which is why we often referred to them as Stateless Function Components. But now, with the use of the useState hook, we can make Function Components behave like Class Components with state. Recently, @types/react also changed SFC to FC.
1.1 Motivation#
The reasons for introducing Hooks are listed in the Introduction to Hooks on the React official website:
- Reusing state logic between components is difficult.
- Complex components become hard to understand.
- Classes are hard to understand.
First, reusing state logic between components is difficult. This has been a topic of discussion in our second article, so I won't elaborate further here.
Second, complex components become hard to understand, meaning the logic of the component is complex. This mainly applies to Class Components, where we often have to write code in various lifecycle methods, such as fetching data in componentDidMount and componentDidUpdate. However, componentDidMount may also include many other logics, making the component increasingly bloated, with logic clearly clustered in various lifecycle functions, turning React development into "lifecycle-oriented programming." The emergence of Hooks transforms this "lifecycle-oriented programming" into "business logic-oriented programming," allowing developers to focus on what they should care about rather than the lifecycle.
Third, classes are hard to understand, as functional programming is simpler than OOP. Now, considering performance, will Hooks slow down because they create functions during rendering? The answer is no; in modern browsers, the performance of closures and classes only shows significant differences in extreme scenarios. Instead, we can argue that the design of Hooks can be more efficient in some aspects:
- Hooks avoid the extra overhead required by classes, such as the cost of creating class instances and binding event handlers in constructors.
- Code that conforms to language habits does not require deep component tree nesting when using Hooks. This phenomenon is very common in codebases that use higher-order components, render props, and context. With a smaller component tree, React's workload decreases accordingly.
In fact, the benefits brought by React Hooks are not only more functional, finer update granularity, and clearer code, but also the following three advantages:
- Multiple states do not create nesting; the writing style remains flat: just as async/await solves callback hell, hooks also solve the nesting hell problem of higher-order components. Although renderProps can also solve this problem through composition, it is slightly cumbersome and increases the number of entities due to the forced encapsulation of a new object.
- Hooks can reference other Hooks, making custom Hooks more flexible.
- It is easier to separate a component's UI from its state.
1.2 Hooks API#
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
- useResponder
The above Hooks APIs will be explained one by one in the future, so I won't elaborate further here. This article will first explain useState.
1.3 Custom Hooks#
By creating custom Hooks, you can extract component logic into reusable functions. Here’s a recommended website: https://usehooks.com/, which collects useful custom Hooks that can be seamlessly integrated into projects, fully demonstrating the strong reusability and simplicity of Hooks.
2. Usage and Rules of useState#
import React, { useState } from 'react'
const App: React.FC = () => {
const [count, setCount] = useState<number>(0)
const [name, setName] = useState<string>('airing')
const [age, setAge] = useState<number>(18)
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => {
setCount(count + 1)
setAge(age + 1)
}}>
Click me
</button>
</>
)
}
export default App
If you have used Redux, this scene will look very familiar. You start with an initial state, then dispatch an action, change the state via a reducer, and return the new state, triggering a re-render of the component.
It is equivalent to the following Class Component:
import React from 'react'
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
age: 18,
name: 'airing'
};
}
render() {
return (
<>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({
count: this.state.count + 1,
age: this.state.age + 1
})}>
Click me
</button>
</>
);
}
}
export default App
As you can see, Function Components are more concise than Class Components, and the use of useState is also very simple. However, it is important to note that the use of Hooks must adhere to this rule: Ensure that Hooks are called in the same order on every render. Therefore, it is best to use Hooks only at the top level each time, and not call Hooks inside loops, conditions, or nested functions, as this can easily lead to errors.
So, why must we adhere to this rule? Next, let's look at the implementation principle of useState and implement a useState ourselves to make it clear.
3. Principles and Simple Implementation of useState#
3.1 Demo 1: dispatch#
In the second section, we found that the usage of useState is quite similar to Redux, so let's implement a useState based on Redux's ideas:
function useState(initialValue) {
let state = initialValue
function dispatch(newState) {
state = newState
render(<App />, document.getElementById('root'))
}
return [state, dispatch]
}
We will replace the useState imported from React with our own implementation:
import React from 'react'
import { render } from 'react-dom'
function useState(initialValue: any) {
let state = initialValue
function dispatch(newState: any) {
state = newState
render(<App />, document.getElementById('root'))
}
return [state, dispatch]
}
const App: React.FC = () => {
const [count, setCount] = useState(0)
const [name, setName] = useState('airing')
const [age, setAge] = useState(18)
return (
<>
<p>You clicked {count} times</p>
<p>Your age is {age}</p>
<p>Your name is {name}</p>
<button onClick={() => {
setCount(count + 1)
setAge(age + 1)
}}>
Click me
</button>
</>
)
}
export default App
At this point, we find that clicking the button does not respond; both count and age do not change. This is because our implementation of useState does not have storage functionality, and every time it re-renders, the previous state is reset. We can think of using an external variable to store it.
3.2 Demo 2: Memoizing state#
Based on this, let's optimize the useState we just implemented:
let _state: any
function useState(initialValue: any) {
_state = _state || initialValue
function setState(newState: any) {
_state = newState
render(<App />, document.getElementById('root'))
}
return [_state, setState]
}
Although the button click now shows changes, the effect is not quite right. If we remove age and name from the useState, we find that the effect is normal. This is because we only used a single variable for storage, which can only store the value of one useState. We can think of using a memoization technique, i.e., an array, to store all states, but we also need to maintain the index of the array.
3.3 Demo 3: Memoization#
Based on this, let's optimize the useState we just implemented again:
let memoizedState: any[] = [] // The values of hooks are stored in this array
let cursor = 0 // The current index of memoizedState
function useState(initialValue: any) {
memoizedState[cursor] = memoizedState[cursor] || initialValue
const currentCursor = cursor
function setState(newState: any) {
memoizedState[currentCursor] = newState
cursor = 0
render(<App />, document.getElementById('root'))
}
return [memoizedState[cursor++], setState] // Return the current state and increment the cursor
}
After clicking the button three times, we print the data of memoizedState as follows:
When the page is initially rendered, each time useState is executed, it will bind the corresponding setState to the corresponding index position, and then store the initial state in memoizedState.
When clicking the button, setCount and setAge will be triggered, and each setState has a reference to its corresponding index, so triggering the corresponding setState will change the value of the state at that position.
Here we are simulating the implementation of useState, so each time we call setState, there is a re-rendering process.
The re-rendering still executes useState in order, but memoizedState already has the previous state value, so the initialized value is not the initial value passed in but the previous value.
Therefore, the answer to the question left in the second section is clear: why do Hooks need to ensure that Hooks are called in the same order on every render? Because memoizedState is populated with data according to the order in which Hooks are defined; if the order of Hooks changes, memoizedState will not be aware of it. Therefore, it is best to use Hooks only at the top level each time, and not call Hooks inside loops, conditions, or nested functions.
Finally, let's see how React implements useState.
4. Source Code Analysis of useState#
4.1 Entry Point#
First, in the entry file packages/react/src/React.js, we find useState, which originates from packages/react/src/ReactHooks.js.
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
resolveDispatcher() returns ReactCurrentDispatcher.current, so useState is actually ReactCurrentDispatcher.current.useState.
So, what is ReactCurrentDispatcher?
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
}
We ultimately find the specific implementation of useState in packages/react-reconciler/src/ReactFiberHooks.js. This file also contains the core processing logic for all React Hooks.
4.2 Type Definitions#
4.2.1 Hook#
Before we start, let's take a look at several type definitions in ReactFiberHooks.js. First, the Hook:
export type Hook = {
memoizedState: any, // Points to the current rendering node Fiber, the final state value after the last complete update
baseState: any, // Initializes initialState, already newState after each dispatch
baseUpdate: Update<any, any> | null, // The current Update that needs to be updated, after each update, will be assigned to the previous update for easy data backtracking in case of rendering errors
queue: UpdateQueue<any, any> | null, // Cached update queue, stores multiple update actions
next: Hook | null, // Links to the next hook, chained through next
};
As we can see, the data structure of Hooks is basically consistent with our previous implementation; memoizedState is also an array. To be precise, React's Hooks are a singly linked list, with Hook.next pointing to the next Hook.
4.2.2 Update & UpdateQueue#
So what are baseUpdate and queue? Let's first look at the type definitions for Update and UpdateQueue:
type Update<S, A> = {
expirationTime: ExpirationTime, // The expiration time of the current update
suspenseConfig: null | SuspenseConfig,
action: A,
eagerReducer: ((S, A) => S) | null,
eagerState: S | null,
next: Update<S, A> | null, // Links to the next Update
priority?: ReactPriorityLevel, // Priority
};
type UpdateQueue<S, A> = {
last: Update<S, A> | null,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
Update is called an update, which will be used when scheduling a React update. UpdateQueue is a queue of Updates, and it also carries the dispatch during updates. The specific process of React Fiber and React update scheduling will not be covered in this article; a separate article will be written later.
4.2.3 HooksDispatcherOnMount & HooksDispatcherOnUpdate#
There are also two Dispatch type definitions that need attention: one is HooksDispatcherOnMount for the first load, and the other is HooksDispatcherOnUpdate for updates.
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useResponder: createResponderListener,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useResponder: createResponderListener,
};
4.3 First Render#
4.3.1 renderWithHooks#
React Fiber starts executing from beginWork() in packages/react-reconciler/src/ReactFiberBeginWork.js (the specific process of React Fiber will be covered in a separate article later). For Function Components, it follows the logic below to load or update components:
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
In updateFunctionComponent, the handling of Hooks is:
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderExpirationTime,
);
Thus, we find that the core entry point for rendering React Hooks is renderWithHooks. We do not care about other rendering processes; this article will focus on renderWithHooks and its subsequent logic.
Let's return to ReactFiberHooks.js to see what renderWithHooks does specifically. Removing error handling code and __DEV__
parts, the code for renderWithHooks is as follows:
export function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
props: any,
refOrContext: any,
nextRenderExpirationTime: ExpirationTime,
): any {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
nextCurrentHook = current !== null ? current.memoizedState : null;
// The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// remainingExpirationTime = NoWork;
// componentUpdateQueue = null;
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
// sideEffectTag = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because nextCurrentHook === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)
// Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
// Non-stateful hooks (e.g. context) don't get added to memoizedState,
// so nextCurrentHook would be null during updates and mounts.
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, refOrContext);
if (didScheduleRenderPhaseUpdate) {
do {
didScheduleRenderPhaseUpdate = false;
numberOfReRenders += 1;
// Start over from the beginning of the list
nextCurrentHook = current !== null ? current.memoizedState : null;
nextWorkInProgressHook = firstWorkInProgressHook;
currentHook = null;
workInProgressHook = null;
componentUpdateQueue = null;
ReactCurrentDispatcher.current = __DEV__
? HooksDispatcherOnUpdateInDEV
: HooksDispatcherOnUpdate;
children = Component(props, refOrContext);
} while (didScheduleRenderPhaseUpdate);
renderPhaseUpdates = null;
numberOfReRenders = 0;
}
// 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-entrancy.
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const renderedWork: Fiber = (currentlyRenderingFiber: any);
renderedWork.memoizedState = firstWorkInProgressHook;
renderedWork.expirationTime = remainingExpirationTime;
renderedWork.updateQueue = (componentUpdateQueue: any);
renderedWork.effectTag |= sideEffectTag;
// This check uses currentHook so that it works the same in DEV and prod bundles.
// hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
currentHook = null;
nextCurrentHook = null;
firstWorkInProgressHook = null;
workInProgressHook = null;
nextWorkInProgressHook = null;
remainingExpirationTime = NoWork;
componentUpdateQueue = null;
sideEffectTag = 0;
// These were reset above
// didScheduleRenderPhaseUpdate = false;
// renderPhaseUpdates = null;
// numberOfReRenders = 0;
return children;
}
renderWithHooks consists of three parts. First, it assigns the value mentioned in 4.1, ReactCurrentDispatcher.current, and then performs didScheduleRenderPhaseUpdate and some initialization work. The core is the first part, so let's take a look:
nextCurrentHook = current !== null ? current.memoizedState : null;
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
If the current Fiber is null, it is considered the first load, and ReactCurrentDispatcher.current.useState will be assigned to HooksDispatcherOnMount.useState; otherwise, it will be assigned to HooksDispatcherOnUpdate.useState. According to the type definitions in 4.2, during the first load, useState = ReactCurrentDispatcher.current.useState = HooksDispatcherOnMount.useState = mountState; during updates, useState = ReactCurrentDispatcher.current.useState = HooksDispatcherOnUpdate.useState = updateState.
4.3.2 mountState#
First, let's look at the implementation of mountState:
// The method actually called when the component's useState is first invoked
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
// Create a new Hook and return the current workInProgressHook
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// Create a new queue
const queue = (hook.queue = {
last: null, // The last update logic, including {action, next} which is the state value and the next Update
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any), // The state during the last render of the component
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// Bind the current fiber and queue.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
}
4.3.3 mountWorkInProgressHook#
mountWorkInProgressHook creates a new Hook and returns the current workInProgressHook, implemented as follows:
// Create a new hook and return the current workInProgressHook
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
// Only when the page is first opened is workInProgressHook null
if (workInProgressHook === null) {
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// If workInProgressHook already exists, attach the newly created Hook to the end of workInProgressHook.
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
4.3.4 dispatchAction#
We notice that mountState also does a crucial thing: it binds the current fiber and queue to dispatchAction:
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// Bind the current fiber and queue
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
Now let's see how dispatchAction is implemented:
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// This branch is for Fiber scheduling during re-render
didScheduleRenderPhaseUpdate = true;
const update: Update<S, A> = {
expirationTime: renderExpirationTime,
suspenseConfig: null,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// Cache the updates generated during this update cycle in renderPhaseUpdates
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
const currentTime = requestCurrentTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
// Store all update actions to calculate the latest state value during the re-render process
const update: Update<S, A> = {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// Append the update to the end of the list.
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
// ... Update the circular linked list
const first = last.next;
if (first !== null) {
// Still circular.
update.next = first;
}
last.next = update;
}
queue.last = update;
// Omitted code for special cases when Fiber is NoWork
// Create an update task to execute the rendering of the fiber
scheduleWork(fiber, expirationTime);
}
}
If the first branch involves Fiber scheduling, we will only mention it here; this article will not explain Fiber in detail. Just know that when fiber === currentlyRenderingFiber
, it is a re-render, meaning a new cycle has been generated during the current update cycle. If it is a re-render, didScheduleRenderPhaseUpdate is set to true, and in renderWithHooks, if didScheduleRenderPhaseUpdate is true, it will loop and count numberOfReRenders to record the number of re-renders; additionally, nextWorkInProgressHook will have a value. Therefore, in the subsequent code, there are checks for whether numberOfReRenders > 0 to determine if it is a re-render, and whether nextWorkInProgressHook is null to determine if it is a re-render.
Also, if it is a re-render, all updates generated during the update process will be recorded in the renderPhaseUpdates Map, using each Hook's queue as the key.
As for the specific work of scheduleWork, we will analyze it in a separate article later.
4.4 Updates#
4.4.1 updateState#
Let's look at the method actually called during the update process of useState, which is updateState:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
// The method actually called every time after the first execution of useState
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
We can see that updateState ultimately calls updateReducer. For the update action triggered by useState, basicStateReducer simply returns the value of action (if action is a function, it will also call it). Therefore, useState is just a special case of useReducer, where the reducer passed is basicStateReducer, responsible for changing the state, unlike useReducer, which can take a custom reducer.
4.4.2 updateReducer#
Now let's see what updateReducer does:
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// Get the currently working hook
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
if (numberOfReRenders > 0) {
// re-render: a new update has been generated during the current update cycle
const dispatch: Dispatch<A> = (queue.dispatch: any);
if (renderPhaseUpdates !== null) {
// All updates generated during the update process are recorded in renderPhaseUpdates Map, using each Hook's queue as the key.
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
let newState = hook.memoizedState;
let update = firstRenderPhaseUpdate;
do {
// If it is a re-render, continue executing these updates until there are no updates in the current render cycle
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
if (hook.baseUpdate === queue.last) {
hook.baseState = newState;
}
queue.lastRenderedState = newState;
return [newState, dispatch];
}
}
return [hook.memoizedState, dispatch];
}
const last = queue.last;
const baseUpdate = hook.baseUpdate;
const baseState = hook.baseState;
let first;
if (baseUpdate !== null) {
if (last !== null) {
last.next = null;
}
first = baseUpdate.next;
} else {
first = last !== null ? last.next : null;
}
if (first !== null) {
let newState = baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
if (!didSkip) {
didSkip = true;
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
if (updateExpirationTime > remainingExpirationTime) {
remainingExpirationTime = updateExpirationTime;
}
} else {
markRenderEventTimeAndConfig(
updateExpirationTime,
update.suspenseConfig,
);
// Loop through the linked list, executing each update
if (update.eagerReducer === reducer) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
if (!didSkip) {
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
updateReducer is divided into two situations:
- Non re-render, meaning there is only one Update in the current update cycle.
- Re-render, meaning a new update has been generated during the current update cycle.
In 4.3.4, we mentioned that numberOfReRenders records the number of re-renders; if it is greater than 0, it indicates that a new update has been generated during the current update cycle, so we continue executing these updates based on the reducer and update.action to create a new state until there are no updates in the current render cycle, and finally assign it to Hook.memoizedState and Hook.baseState.
Note: In fact, when using useState alone, it is almost impossible to encounter a re-render scenario unless setState is directly written at the top of the function, but this would lead to infinite re-renders, and numberOfReRenders would exceed the limit, causing the program to throw an error in 4.3.4 (the part related to
__DEV__
and this error handling code is omitted):
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
Now let's look at the non-re-render situation. Excluding the Fiber-related code and special logic, the key point is the do-while loop, which is responsible for looping through the linked list and executing each update:
do {
// Loop through the linked list, executing each update
if (update.eagerReducer === reducer) {
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
One more thing to note is that in this case, each update needs to be prioritized; if it is not within the current overall update priority, it will be skipped. The first skipped update will become the new Hook.baseUpdate. This ensures that subsequent updates are executed based on the baseUpdate. The specific logic here will be analyzed in a separate article later. Finally, it also needs to be assigned to Hook.memoizedState and Hook.baseState.
4.4.3 updateWorkInProgressHook#
Here, let's add that the way to get the Hook in the first line of code is different from mountState; updateWorkInProgressHook gets the currently working Hook. Its implementation is as follows:
// Get the currently working Hook, i.e., workInProgressHook
function updateWorkInProgressHook(): Hook {
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
nextCurrentHook = currentHook !== null ? currentHook.next : null;
} else {
// Clone from the current hook.
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
queue: currentHook.queue,
baseUpdate: currentHook.baseUpdate,
next: null,
};
if (workInProgressHook === null) {
workInProgressHook = firstWorkInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
nextCurrentHook = currentHook.next;
}
return workInProgressHook;
}
This is divided into two situations. In 4.3.4, we mentioned that if nextWorkInProgressHook exists, it indicates a re-render; if it is a re-render, it means that the current update cycle still needs to process workInProgressHook.
If it is not a re-render, we take the next Hook as the current Hook, and at the same time, like in 4.3.3 mountWorkInProgressHook, we create a new Hook and return workInProgressHook.
In summary, updateWorkInProgressHook obtains the currently working workInProgressHook.
5 Conclusion#
To be more intuitive, I captured a data structure of a Hook in operation, as shown in the following image:
To summarize the process analyzed in the article, see the following image:
If you still have doubts about the source code of useState, you can write a small Demo and debug at the key functions.