Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Everything about useState


May 31, 2021 Article blog


Table of contents


The following article comes from the public number: Magician Kasong, author Kasong

As a React developer, can you answer the following two questions:

  1. For the following function components:

function App() {
  const [num, updateNum] = useState(0);
  window.updateNum = updateNum;
  return num;
}

Can calling window.updateNum(1) update 0 in the view to 1

  1. For the following function components:

function App() {
  const [num, updateNum] = useState(0);

  
  function increment() {
    setTimeout(() => {
      updateNum(num + 1);
    }, 1000);
  }

  
  return <p onClick={increment}>{num}</p>;
}

Quickly tap p 5 times in 1 second, what does the view look like?

1. 可以
2. 显示为1

In fact, these two questions are essentially asking:

  • How useState save the state?
  • How useState update status?

This article combines the source code to cover the above two questions.

These are all you need to know about useState

How hook saves data

FunctionComponent render itself is just a function call.

So how does the hook called inside the render get the corresponding data?

Like what:

  • useState gets state
  • useRef gets ref
  • useMemo gets cached data

The answer is:

Each component has a corresponding fiber节点 (which can be understood as a 虚拟DOM that holds information about the component.

Each time FunctionComponent render the global variable currentlyRenderingFiber is assigned a fiber节点 for that FunctionComponent

Therefore, the inside of hook actually gets status information from currentlyRenderingFiber

How multiple hooks get data

We know that there may be multiple hook in a FunctionComponent such as:

function App() {
  // hookA
  const [a, updateA] = useState(0);
  // hookB
  const [b, updateB] = useState(0);
  // hookC
  const ref = useRef(0);

  
  return <p></p>;
}

How do so many hook get their own data?

The answer is:

A one-way list of hook data is saved in currentlyRenderingFiber.memoizedState

As in the example above, it can be understood as:

const hookA = {
  // hook保存的数据
  memoizedState: null,
  // 指向下一个hook
  next: hookB
  // ...省略其他字段
};


hookB.next = hookC;


currentlyRenderingFiber.memoizedState = hookA;

When FunctionComponent render each hook is executed, the pointer to the currentlyRenderingFiber.memoizedState list moves backward once, pointing to the current hook corresponding data.

This is why React requires that hook cannot be called in a order that cannot be changed hook cannot be used in conditional statements) -- the hook corresponding data is obtained from a fixed-order list each time the order is render

 Everything about useState1

UseState executes the process

We know that useState returns the second argument of the array of values as the method of changing the state.

In the source code, he is called dispatchAction

Whenever dispatchAction called, an object update is created that update an update:

const update = {
  // 更新的数据
  action: action,
  // 指向下一个更新
  next: null
};

For examples below

function App() {
  const [num, updateNum] = useState(0);

  
  function increment() {
    updateNum(num + 1);
  }

  
  return <p onClick={increment}>{num}</p>;
}

Calling updateNum(num + 1) creates:

const update = {
  // 更新的数据
  action: 1,
  // 指向下一个更新
  next: null
  // ...省略其他字段
};

If dispatchAction is called more than once, for example:

function increment() {
  // 产生update1
  updateNum(num + 1);
  // 产生update2
  updateNum(num + 2);
  // 产生update3
  updateNum(num + 3);
}

update forms a ringed list.

update3 --next--> update1
  ^                 |
  |               update2
  |______next_______|

                          

Where is this list kept?

Since this update list is generated by a dispatchAction it is clear that the list belongs to the useState hook useState

We continue to supplement hook data structure.

const hook = {
  // hook保存的数据
  memoizedState: null,
  // 指向下一个hook
  next: hookForB
  // 本次更新以baseState为基础计算新的state
  baseState: null,
  // 本次更新开始时已有的update队列
  baseQueue: null,
  // 本次更新需要增加的update队列
  queue: null,
};

Where, the list of update for this update is saved in queue

When state the ring list of queue is cut and mounted on the last face of baseQueue baseQueue the new state based on baseState

When the state is complete, the new state memoizedState

 Everything about useState2

Why updates are not based on memoizedState but baseState state because the state calculation process needs to consider priority, and some update priorities may not be skipped. So memoizedState is not necessarily the same as baseState

Back to our opening question:

function App() {
  const [num, updateNum] = useState(0);
  window.updateNum = updateNum;
  return num;
}

Can calling window.updateNum(1) update 0 in the view to 1

We need to look at the implementation of the updateNum approach here:

updateNum === dispatchAction.bind(null, currentlyRenderingFiber, queue);

As you can see, updateNum method binds the dispatchAction of currentlyRenderingFiber to queue hook.queue

As described above, the purpose of calling dispatchAction is to generate update and insert them into the hook.queue list.

Now that queue is bound to dispatchAction as a preset parameter, the call dispatchAction is limited to FunctionComponent

Update's action

The second question

function App() {
  const [num, updateNum] = useState(0);

  
  function increment() {
    setTimeout(() => {
      updateNum(num + 1);
    }, 1000);
  }

  
  return <p onClick={increment}>{num}</p>;
}

Quickly tap p 5 times in 1 second, what does the view look like?

We know that calling updateNum produces update where the transpose update.action

Click 5 times in 1 second. When you click on the fifth time, the update update by the first click has not entered the update process, so hook.baseState has not changed.

The update generated by these five clicks are all based on the same baseState calculation of the new state and num variable has not changed (i.e. 5 update.action (i.e., num + 1 is the same value).

Therefore, the final rendering result is 1.

UseState and useReducer

So how do you change the view from 1 to 5 with 5 clicks?

From what we know above, we need to change baseState or action

baseState determined by React's update process, we have no control over it.

But we can control action

action can pass not only but also 函数

// action为值
updateNum(num + 1);
// action为函数
updateNum(num => num + 1);

In the process of generating a new state based on baseState and update lists:

let newState = baseState;
let firstUpdate = hook.baseQueue.next;
let update = firstUpdate;


// 遍历baseQueue中的每一个update
do {
  if (typeof update.action === 'function') {
    newState = update.action(newState);
  } else {
    newState = action;
  }
} while (update !== firstUpdate)

It can be seen that when passing since we action same value for action 5 times, the newState that is ultimately calculated is also the same value.

When passing 函数 newState is calculated five times based on the action function, and the result is finally added up.

If in this example, we use useReducer of useState because useReducer action is always a 函数 we will not encounter the problem in our example.

In fact, useState itself is a useReducer preset with the following reducer

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

summary

Through this article, we learned about the full execution of useState

That's what W3Cschool编程狮 has to say about useState, and I hope it will help you.