preface

The React source reference version is 17.0.3. This is the React source series second, it is recommended that first look at the source of the students from the first to see, so more coherent, there are source series links below.

The title reads “useState, useReducer”, but this article only talks about useState, because in the source code, useState uses the same code as useReducer, so there is no additional detail.

The warm-up to prepare

Before we officially talk about useState, let’s warm up and learn what we need to know.

Why are there hooks

You know that hooks are products of function components. Why didn’t class components have hooks before?

The simple answer is no.

This is because in a class component, only one instance is generated at run time, where information such as the state of the component is stored. In subsequent updates, the render method is also called, and the information in the instance is not lost. In the function component, each rendering and update will execute the function component, so there is no way to save the state information in the function component. To save information such as state, there are hooks that record the state of function components and execute side effects.

Hooks execution time

As mentioned above, in the function component, each render and update will execute the function component. Therefore, the hooks that we declare inside function components are also executed each time the function is executed.

It is important to know that it is not only a function that is executed, but also that it is not a function that is executed, that it is not a function that is executed, that it is a function that is executed.

The answer is, recorded in the corresponding fiber node of the function component.

Two sets of hooks

When we first learned to use hooks, we might have wondered why hooks are declared at the top of function components, but not in conditional statements or internal functions.

The answer is that React maintains two sets of hooks, one of which is used to initialize hooks when a project initializes a mount. Subsequent updates are performed based on the initialized hooks. If we declare hooks in a conditional statement or function, it is possible that they will not be declared when the project is initialized, which can cause problems later in the update operation.

Hooks storage

I want to talk about the hooks in advance, so that I don’t get dizzy

Each initialized hook creates a hook structure, and the multiple hooks are associated in a linked list by declaring order. This list will eventually be stored in fiber.memoizedState:

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Next: null // link to the next hook};Copy the code

Each update stored in each hook. Queue is also a linked list structure, do not be confused with the hook list.

Next, let’s look at the article with the following questions:

  1. whysetStateCan’t get the latest right awaystateThe value of?
  2. multiplesetStateHow did it merge?
  3. setStateIs it synchronous or asynchronous?
  4. whysetStateIs the function component not updated when the value of is the same?

Suppose we have the following code:

function App(){ const [count, SetCount] = useState(0) const handleClick = () => {setCount(count => count + 1)} return (<div> <span style = "box-sizing: border-box! Important; word-wrap: break-word! Important; word-wrap: break-word! Important;"Copy the code

Initialize the mount

useState

Let’s first look at the useState() function:

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
Copy the code

InitialState is the parameter we pass to useState, which can be either the underlying data type or a function. We’ll focus on the dispatcher.usestate (initialState) method, because we’re initializing here, and it calls the mountState method:

function mountState(initialState) { var hook = mountWorkInProgressHook(); // workInProgressHook if (typeof initialState === 'function') { InitialState = initialState(); } hook.memoizedState = hook.baseState = initialState; var queue = hook.queue = { pending: null, dispatch: null, lastRenderedReducer: basicStateReducer, lastRenderedState: initialState }; var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue); return [hook.memoizedState, dispatch]; }Copy the code

The code above is relatively simple. It generates a queue based on the incoming parameters of useState() and saves it in the hook. Then it exposes the incoming parameters and the dispatchAction bound with the two parameters to the function component as the return value.

The two return values, the first hook. MemoizedState is easy to understand, it’s the initial value, the second dispatch, So dispatchAction.bind(null, currentlyRenderingFiber$1, queue) what is that? We know that using the useState() method returns two values state and setState, and this setState corresponds to the dispatchAction above, so how does this function help us set the value of state?

Let’s keep that in mind, read on, and find out the answer later.

Let’s focus on what mountWorkInProgressHook has done.

mountWorkInProgressHook

function mountWorkInProgressHook() { var hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null }; If (workInProgressHook === null) {currentlyRenderingFiber$1. MemoizedState = $1 workInProgressHook = hook; } else {// add the hook to the last hook, and pointer to the hook: workinProgreshooked. next = hook; } return workInProgressHook; }Copy the code

MemoizedState = workInProgressHook = hook; In this line of code, we can see that the hook is stored on the corresponding fiber. MemoizedState.

workInProgressHook = workInProgressHook.next = hook; From this line of code, we can know that if there are multiple hooks, they are stored in the form of linked list.

Not only is useState() initialized using mountWorkInProgressHook, but other hooks, such as useEffect, useRef, useCallback, and so on, are initialized using this method.

Here we can make two things clear:

  • hooksState data is stored in the corresponding function componentfiber.memoizedState;
  • If there are more than one function componenthook, they will passOrder of declarationStored in a linked list structure;

At this point, our useState() has done all the work of initializing it. In brief, useState() initializes by storing the initial value we pass in the hook structure to the corresponding fiber. Returns [state, dispatchAction] as an array.

Update the update

When we trigger setState() in some form, React also decides how to update the view based on the value of setState().

As mentioned above, useState will return [state, dispatchAction] when initialized, so we call setState(), which is actually calling dispatchAction, and this function is initialized with two arguments bound by bind. One is the fiber corresponding to the function component when useState is initialized, and the other is the queue of the hook structure.

So let’s take a look at my simplified dispatchAction.

Function dispatchAction(fiber, queue, action) {var update = {lane: lane, action: action, eagerReducer: null, eagerState: null, next: null }; // Insert the update into the closed list. var pending = queue.pending; if (pending === null) { update.next = update; } else { update.next = pending.next; pending.next = update; } queue.pending = update; var alternate = fiber.alternate; / / to determine whether the current render phase the if (fiber. The lanes = = = NoLanes && (alternate = = = null | | alternate. Lanes = = = NoLanes)) {var lastRenderedReducer = queue.lastRenderedReducer; If (lastRenderedReducer!); if (lastRenderedReducer!); == null) { var prevDispatcher; { prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV; } try { var currentState = queue.lastRenderedState; var eagerState = lastRenderedReducer(currentState, action); update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (objectIs(eagerState, currentState)) { return; } } finally { { ReactCurrentDispatcher$1.current = prevDispatcher; ScheduleUpdateOnFiber (fiber, lane, eventTime); }}Copy the code

The above code is as simple as I can make it… There are comments on the code, you look at the official make do.

Without going into detail, let me summarize what dispatchAction does:

  • To create aupdateTo join thefiber.hook.queueIn the list, and the list pointer points to thisupdate;
  • Determine if the current rendering phase determines whether to schedule updates immediately;
  • Check whether the current operation is the same as the last operation. If the operation is the same, the update is not scheduled.
  • The above conditions will be met withupdatethefiberScheduling update;

So here’s another thing that we figured out:

whysetStateIs the function component not updated when the value of is the same?

updateState

We won’t go into the details of the process of scheduling updates here, but we need to know that during the next update, our function component will be executed again, and the useState method will be called again. As mentioned earlier, React maintains two sets of hooks, one for initialization and one for update. This is done when the update is scheduled. So we’re going to call the useState method differently this time than we did before.

So this time we go into useState, and we see that it’s actually calling the updateState method

function updateState(initialState) {
  return updateReducer(basicStateReducer);
}
Copy the code

Seeing these lines of code, readers should understand why some people on the Internet say that useState and useReducer are similar. UpdateReducer: updateReducer: updateReducer: updateReducer

updateReducer

It was very long, I want you to bear it. The pain was reduced a lot

Function updateReducer(Reducer, initialArg, init) {// create a new hook, Update var hook = updateWorkInProgressHook(); var queue = hook.queue; queue.lastRenderedReducer = reducer; var current = currentHook; var baseQueue = current.baseQueue; var pendingQueue = queue.pending; current.baseQueue = baseQueue = pendingQueue; if (baseQueue ! == null) {// Insert the update into the closed list. Update var first = basequeue.next; var newState = current.baseState; var update = first; SetState do {var updateLane = update.lane; Var action = update.action; var action = update.action; var action = update.action; NewState = Reducer (action) newState = Reducer (action); update = update.next; } while (update ! == null && update ! == first); hook.memoizedState = newState; hook.baseState = newBaseState; hook.baseQueue = newBaseQueueLast; queue.lastRenderedState = newState; } var dispatch = queue.dispatch; return [hook.memoizedState, dispatch]; }Copy the code

In the previous update, we would loop through the update to do a merge operation, and only take the value of the last setState. At this point, one might ask, wouldn’t it be easier to just take the value of the last setState?

This is not possible because the setState input can be either a base type or a function. If the setState input is a function, it will rely on the previous setState value to complete the update operation. The following code is the reducer in the above loop

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}
Copy the code

Now, one of the things we’ve figured out here is, how do setstates merge?

updateWorkInProgressHook

The following is the pseudocode, I have deleted a lot of logical judgments, so as not to be too long and make you uncomfortable. In the original code, it will judge whether the current hook is the first one that is scheduled to be updated, so HERE I will parse the first one for simplicity

function updateWorkInProgressHook() {
  var nextCurrentHook;

  nextCurrentHook = current.memoizedState;

  var newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null
      }
      
  currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;

  return workInProgressHook;
}
Copy the code

As you can see from the code above, the updateWorkInProgressHook throws those judgments, and what it does is very simple: create a new hook structure based on fiber.memoizedState and overlay the previous hook structure. In the previous section of dispatchAction we talked about adding an update to hook.queue, and here we have the update on newhook.queue.

conclusion

To summarize the useState initialization and setState update:

  1. useStateIs initialized the first time a function component is executed and returns[state, dispatchAction].
  2. When we go throughsetStateThat isdispatchActionWhen a scheduled update is made, one is createdupdateTo join thehook.queueIn the.
  3. Is also called when the function component is executed again during the update processuseStateMethod, at this pointuseStateThe internal will use the updatehooks.
  4. throughupdateWorkInProgressHookAccess to thedispatchActionTo create theupdate.
  5. inupdateReducerBy iterating throughupdateList completesetStateA merger.
  6. returnupdateAfter the[newState, dispatchAction].

Two more questions

  1. whysetStateCan’t get the latest right awaystateThe value of?

React could do this, but why not? Because every setState triggers an update. React does a merge for performance reasons. So setState just triggers dispatchAction to generate an update action. The new state will be stored in the update and will not be assigned until the next render triggers the execution of the useState function component.

  1. setStateIs it synchronous or asynchronous?

Synchronous, if we have a code like this:

const handleClick = () => {
  setCount(2)
  setCount(count => count + 1)
  console.log('after setCount')
}
Copy the code

You’ll be surprised to find that the page hasn’t updated count yet, but the console has printed after setCount.

It looks asynchronous because of the internal use of the try{… }finally{… }. When a call to setState triggers a scheduled update, the update operation is placed in finally, returning the logic to continue executing the handlelick. So this is what happens.

After reading this article, we can understand the following questions:

  1. whysetStateCan’t get the latest right awaystateThe value of?
  2. multiplesetStateHow did it merge?
  3. setStateIs it synchronous or asynchronous?
  4. whysetStateIs the function component not updated when the value of is the same?
  5. setStateHow is the update done?
  6. useStateWhen is it initialized and when is it updated?

Article series arrangement:

  1. React Fiber;
  2. React source series 2: React rendering mechanism;
  3. React source series 3: hooks useState, useReducer;
  4. React code series 4: hooks useEffect;
  5. React code Series 5: Hooks useCallback, useMemo;
  6. React source Series 6: Hooks useContext;
  7. React source series 7: React synthesis events;
  8. React source series eight: React diff algorithm;
  9. React source series 9: React update mechanism;
  10. React source series 10: Concurrent Mode;

Reference:

React official documents;

Making;