- React source version: V16.11.0
- Source note: Airingursb /react
Before writing this article, I read some articles on the Internet. The source code of the Hooks is either too simple or not detailed, so this article focuses on the source code, from the shallow to the deep, try to not let go of a line of code. The first Hooks in this series is useState. We’ll start with the use of useState, then explain the rules, explain the principles, then simply implement them, and finally source code parsing. In addition, at the beginning of this article, add an overview of Hooks, which were not included in the first two articles due to space problems.
Note: It’s been two months since the last article, it’s been busy these two months, so I didn’t have time to update this series of articles, but react was updated from 16.9 to 16.11 in these two months, I reviewed these changes, which didn’t involve hooks. So I also directly updated the source notes to 16.11.
1. React Hooks
Hooks are a new feature in React 16.8 that lets you use state and other React features without having to write classes. They are essentially a special Class of functions that start with “use” to inject functionality into the Function Component, giving it some of the capabilities of a Class Component.
For example, we used to say that Function Component cannot save state, so we often say Stateless Function Component, But now we can use the useState hook to make the Function Component have state just like the Class Component. Some time ago @types/react also changed SFC to FC.
1.1 motivation
The reasons for launching Hook are listed in the Introduction of Hook on React website:
- It is difficult to reuse state logic between components
- Complex components become difficult to understand
- Unintelligible class
First, it is difficult to reuse state logic between components. This is an issue that we have been discussing in the second part of this series and will not cover here.
Second, complex components become difficult to understand, that is, the component logic is complex. In the case of Class Component, we often need to write code in various components lifecycle, such as componentDidMount and componentDidUpdate to get data, But there may be a lot of other logic included in componentDidMount, which makes component development more bloated, and the logic is noticeably clustered in various lifecycle functions, making React development “lifecycle programming.” The introduction of Hooks turned this “lifecycle programming” into “business logic programming,” eliminating the need for developers to care about the lifecycle they shouldn’t care about.
Third, unintelligible classes, manifested in the fact that functional programming is much simpler than OOP. So dig a little deeper into performance, will hooks be slowed down by creating functions at render time? The answer is no. In today’s browsers, the raw performance of closures and classes differs significantly only in extreme scenarios. Instead, we can think that Hook design will be more efficient in some aspects:
- Hooks avoid the extra overhead of classes, such as the cost of creating class instances and binding event handlers in constructors.
- Language-conforming code does not need to be deeply nested in the component tree when using hooks. This phenomenon is very common in code bases that use high-level components, render props, and context. The component tree is smaller and React work is reduced.
In fact, React Hooks provide not only more functional, fine-grained updates, and cleaner code, but also three benefits:
- Multiple states are not nested, and the writing is tiled: hooks solve the nesting hell problem for higher-order components, just as async/await does for callback hell. While renderProps can also solve this problem with compose, it’s a bit cumbersome to use, and it increases the number of entities by forcing the encapsulation of a new object.
- Hooks can refer to other Hooks, and custom Hooks are more flexible.
- It is easier to separate the UI of a component from its state.
1.2 Hooks API
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useImperativeHandle
- useLayoutEffect
- useDebugValue
- useResponder
All of the above Hooks APIS will be covered in the future and will not be covered here. This article starts with useState.
1.3 Custom Hooks
Custom hooks allow you to extract component logic into reusable functions. Amway has a website, usehooks.com/, that collects useful, custom Hooks that you can use seamlessly in your projects, demonstrating how reportable they are.
2. Usage and rules of useState
import React, { useState } from 'react'
const App: React.FC = (a)= > {
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
Copy the code
If you’ve ever used redux, this scene will look very familiar. Given an initial state, then change the state via dispatch an action, return the new state via Reducer, and trigger the component to re-render.
It is equivalent to the following Class Component:
import React from 'react'
class Example 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>
</>); }}Copy the code
As you can see, Function Component needs to be cleaner than Class Component, and useState is very simple to use. It is important to note that the use of Hooks must comply with this rule: make sure that Hooks are called in the same order every time that they are rendered. Therefore, it is best to use Hooks only at the top level at a time, and not to call Hooks in loops, conditions, or nested functions.
So why do we have to comply with this rule? Next, let’s take a look at the implementation principle of useState and implement a useState yourself at a glance.
3. The principle and simple implementation of useState
3.1 Demo 1: dispatch
In section 2 we found that useState is used like 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]
}
Copy the code
We’ll replace the useState we introduced in React with our own:
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
Copy the code
At this point, we noticed that clicking the button did not respond at all. Neither count nor age changed. Since our implementation of useState does not have storage, the state is reset each time we rerender. And the idea here is to store it externally in a variable.
3.2 Demo 2: Memory 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]
}
Copy the code
The button click is different, but it’s not quite right. If we delete the age and the name of the useState we’ll see that the effect is normal. This is because we only use a single variable to store, which naturally only stores the value of the useState. So we thought we could use a memo, which is an array, to store all the states, but at the same time we need to maintain the index of the array.
3.3 Demo 3: Memo
Based on this, let’s optimize the useState we just implemented again:
let memoizedState: any[] = [] // The hooks values are stored in this array
let cursor = 0 // Index of current 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[catalog ++], setState]Copy the code
After we click the button three times, we print out memoizedState’s data as follows:
Open the page for initial rendering, bind the corresponding setState to the location of the corresponding index each time useState executes, and then store the initial state in memoizedState.
When the button is clicked, setCount and setAge are triggered. Each setState has a reference to its corresponding index, so firing the corresponding setState changes the value of the state at the corresponding position.
This is a mock implementation of useState, so there is a re-render every time setState is called.
The rerendering is still done in turn with useState, but there is already the last state value in memoizedState, so the initial value is not the one passed in but the last value.
So the answer to the question left behind in section 2 is obvious: why do Hooks need to ensure that they are called in the same order every time they are rendered? Because memoizedState places the data in the order that the Hooks define, memoizedState will not be aware if the order of the Hooks changes. Therefore, it is best to use Hooks only at the top level at a time and not to call Hooks inside loops, conditions, or nested functions.
Finally, let’s look at how useState is implemented in React.
4. Analyze the useState source code
4.1 the entrance
First packages in the entrance to the file/react/SRC/react. We find useState in js, it originated in the packages/react/SRC/ReactHooks. Js.
export function useState<S> (initialState: (() = >S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
Copy the code
ResolveDispatcher () returns the ReactCurrentDispatcher. Current, so useState is ReactCurrentDispatcher. Current. UseState.
So, what is ReactCurrentDispatcher?
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
}
Copy the code
We finally found the packages/react – the reconciler/SRC/ReactFiberHooks js, here have useState concrete implementation. This file also contains the core processing logic for all React Hooks.
4.2 Type Definition
2 hooks
Before we begin, let’s look at the definitions of a few types in reactFiberhook.js. First, Hooks:
export type Hook = {
memoizedState: any, // Point to the final state value of the current render node Fiber since the last full update
baseState: any, // Initialize initialState, which has been newState after each dispatch
baseUpdate: Update<any, any> | null.Update = Update = Update = Update = Update = Update = Update = Update = Update = Update
queue: UpdateQueue<any, any> | null.// A cached update queue that stores the behavior of multiple updates
next: Hook | null.// Link to the next hooks, concatenating each with next
};
Copy the code
MemoizedState is also an array. To be exact, React Hooks are a one-way linked list, with Hooks. Next pointing to the next Hook.
4.2.2 the Update & UpdateQueue
So what about baseUpdate and queue? Let’s 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.// link Next Updatepriority? : ReactPriorityLevel,/ / priority
};
type UpdateQueue<S, A> = {
last: Update<S, A> | null.dispatch: (A= > mixed) | null.lastRenderedReducer: ((S, A) = > S) | null.lastRenderedState: S | null};Copy the code
An Update is called an Update and is used when scheduling a React Update. UpdateQueue is a queue for updates, along with dispatches for updates. The specific process of React Fiber and React update scheduling will not be involved in this article, and there will be a separate article to explain it later.
Holdings HooksDispatcherOnMount & HooksDispatcherOnUpdate
There are two Dispatch type definitions that need to be looked at: HooksDispatcherOnMount when first loaded, and HooksDispatcherOnUpdate when updated.
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,
};
Copy the code
4.3 First Rendering
4.3.1 renderWithHooks
Will React Fiber from packages/React – the reconciler/SRC/ReactFiberBeginWork beginWork of js () start (the React of Fiber concrete process subsequent written supplementary explanations alone). For a Function Component, the following logic is used to load or update the Component:
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,
);
}
Copy the code
In updateFunctionComponent, the handling for Hooks is:
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderExpirationTime,
);
Copy the code
Therefore, we find that the core render entry for React Hooks is renderWithHooks. While we don’t care about other render processes, we’ll focus on renderWithHooks and the logic that follows.
Let’s go back to reactFiberhooke.js and see exactly what renderWithHooks do. Remove the fault-tolerant code and the __DEV__ part. The renderWithHooks code looks like this:
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 listnextCurrentHook = 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.
constdidRenderTooFewHooks = 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;
}
Copy the code
RenderWithHooks consists of three parts, the first is the assignment of ReactCurrentDispatcher mentioned in 4.1. The current and subsequent didScheduleRenderPhaseUpdate is made, and some initialization work. The core is the first part, let’s look at it:
nextCurrentHook = current ! = =null ? current.memoizedState : null;
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
Copy the code
If the current Fiber is empty, that is the first time loading, ReactCurrentDispatcher. Current. UseState assignment into HooksDispatcherOnMount. UseState, Otherwise the assignment HooksDispatcherOnUpdate useState. According to the type definition in 4.2, that is first loaded, useState = ReactCurrentDispatcher. Current. UseState = HooksDispatcherOnMount. UseState = mountState; Update useState = ReactCurrentDispatcher. Current. UseState = HooksDispatcherOnUpdate. UseState = updateState.
4.3.2 mountState
First look at the implementation of mountState:
/ / for the first time the method of actual call the function when the calling component useState 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 const queue = (hook. Queue = {last: null, // last Update logic, including {action, next} = status value and the next Update dispatch: Null, lastRenderedReducer: basicStateReducer, lastRenderedState: (initialState: any), // State when the last component was rendered}); const dispatch: Dispatch< BasicStateAction<S>, > = (queue.dispatch = (dispatchAction.bind( null, ((currentlyRenderingFiber: any): fiber), queue,): any)); return [hook.memoizedState, dispatch]; }Copy the code
4.3.3 mountWorkInProgressHook
MountWorkInProgressHook creates a new Hook and returns the current workInProgressHook.
// 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 the first time you open the page, the workInProgressHook is empty
if (workInProgressHook === null) {
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// Connect the newly created Hook to the end of the workInProgressHook that already exists.
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
Copy the code
4.3.4 dispatchAction
Note that mountState also does one critical thing: bind the current fiber and queue to the dispatchAction:
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null.// Bind the current fiber and queue
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
Copy the code
So let’s see how dispatchAction is implemented:
function dispatchAction<S.A> (fiber: Fiber, queue: UpdateQueue
, action: A,
,>) {
const alternate = fiber.alternate;
if( fiber === currentlyRenderingFiber || (alternate ! = =null && alternate === currentlyRenderingFiber)
) {
// This branch is the Fiber scheduling process for re-render
didScheduleRenderPhaseUpdate = true;
const update: Update<S, A> = {
expirationTime: renderExpirationTime,
suspenseConfig: null,
action,
eagerReducer: null.eagerState: null.next: null};// Cache the updates from this update cycle into renderPhaseUpdates
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map(a); }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 behavior so that the latest status value can be calculated in 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 list
const first = last.next;
if(first ! = =null) {
// Still circular.
update.next = first;
}
last.next = update;
}
queue.last = update;
// Omit the code for the special case Fiber NoWork
// Create an update task to render the fiberscheduleWork(fiber, expirationTime); }}Copy the code
The first branch of if is related to the scheduling of fibers, which we will only mention here. This article will not explain in detail the Fiber, as long as the Fiber === currentlyRenderingFiber is re-render, that is, the current update cycle generated a new cycle. If they are re – render, didScheduleRenderPhaseUpdate set to true, and if in renderWithHooks didScheduleRenderPhaseUpdate is true, NumberOfReRenders will count the number of re-render times; NextWorkInProgressHook will also have a value. NumberOfReRenders > 0 is used to determine whether it is re-render or not. NumberOfReRenders > 0 is also used to determine whether it is re-render or not.
At the same time, if it is re-render, all updates generated in the update process will be recorded on the renderPhaseUpdates Map, with the queue of each Hook as the key.
As for the specific work of the final scheduleWork, we will analyze it separately later.
The 4.4 update
4.4.1 updateState
Let’s take a look at the actual method called updateState when we update useState:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { return typeof action === 'function' ? action(state) : action; } / / for the first time after each time the method of performing useState actual call function updateState < S > (initialState: () () = > S | S,) : [S, Dispatch<BasicStateAction<S>>] { return updateReducer(basicStateReducer, (initialState: any)); }Copy the code
And you can see that what updateState actually ends up calling is updateReducer. BasicStateReducer for update actions triggered by useState, basicStateReducer simply returns the value of the action (or if the action is a function). Therefore, useState is only a special case of useReduer, and its reducer is passed in as the basicStateReducer, which changes state, rather than as the useReducer, which can pass in a custom reducer.
4.4.2 updateReducer
So let’s see what updateReducer does:
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; If (numberOfReRenders > 0) {// re-render: new updates are generated in the current update cycle const Dispatch: dispatch <A> = (queue.dispatch: any); if (renderPhaseUpdates ! == null) {// All updates generated during the update process are recorded in the renderPhaseUpdates Map with the queue of each Hook 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 re-render, continue to perform 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, ); 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]; }Copy the code
UpdateReducer is divided into two cases:
- Non-re-render, meaning there is only one Update in the current Update cycle.
- Re-render, the current update cycle has generated a new update.
In 4.3.4, we mentioned that numberOfReRenders records the number of re-render times. If the number is greater than 0, it means that new updates have been generated in the current update cycle, then these updates will continue to be performed. Create the new state based on the reducer and update.action and assign it to hook.memoizedState and hook.basestate at last until there is no update in the current rendering cycle.
Note: In fact, using useState alone you will rarely encounter re-render unless you write setState directly at the top of the function, but this will result in infinite re-render, numberOfReRenders will break the limit, Let the program report errors in 4.3.4 dispatchAction (4.3.4 omitted __DEV__ and this part of the fault-tolerant code) :
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',);Copy the code
So let’s take a look at the non-re-render case, minus the fiber-related code and special logic, and focus on the do-while loop, which loops through the list and performs each update:
do {
// Loop the list, performing 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);Copy the code
Another important point to note is that in this case, you need to determine the priority of each update. Updates that are not in the current overall priority will be skipped, and the first skipped update will become the new Hook. BaseUpdate. You need to ensure that subsequent updates are performed on top of the baseUpdate update, so the results may be different. The specific logic here will be documented separately later. Finally, you also need to assign to Hook. MemoizedState and Hook. BaseState.
4.4.3 updateWorkInProgressHook
As a footnote, notice that the first line of code gets hooks differently than mountState. UpdateWorkInProgressHook gets hooks that are currently working. The implementation is as follows:
// Get the Hook that is currently working, that is, the 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;
}
Copy the code
There are two cases. In 4.3.4 we mentioned that if the nextWorkInProgressHook exists then it is re-render. If it is re-render, it means that the workInProgressHook will continue to be processed during the current update cycle.
If not re – render, is to remove a Hook for the current Hook, at the same time as 4.3.3 mountWorkInProgressHook, create a Hook and returns workInProgressHook.
In summary, updateWorkInProgressHook gets the workInProgressHook in the current work.
5 conclusion
To make it more intuitive, I truncated the data structure of a Hook in operation, as shown in the figure below:
To summarize the process analyzed above, it is shown in the figure below:
If you’re still confused about the useState source code, you can write your own small Demo to debug the key function break points.