This article refers to the latest React 18 Alpha, but the Hooks principle has not changed much since they were “born”.

The principles of useState are not complicated at all, and I will try to make them accessible to anyone who has used React.

Anyway, today let me take you to read its source code and sort out the principle of useState.

Here is one of our functional components:

function App() {
  const [count, setCount] = useState(0)

  function handleClick() {
    setCount((count) = > {
      return count + 1; })}return (
    <button type="button" onClick={handleClick}>
        count is: {count}
    </button>)}Copy the code
  1. This component initializes count to 0 and renders it on the page;

  2. When we click the button, the setCount action is triggered, and the whole FunctionComponent is called again, updating the count to 1.

Our presentation today will also follow the above process, divided into two parts: the first part, first introduce the initial rendering situation; The second part describes how components are updated.

Suppose we use two usEstates inside the component:

const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(10)
Copy the code

If we want to render the App component, simply put, we execute the App function once and get the return value to render.

In React, the ClassComponent, FunctionComponent, etc., correspond to an object called Fiber to store its various node state information, which is also the virtual DOM object in React. Our current App component is no exception and will also correspond to a Fiber object.

There is a property on the Fiber object that records the internal State object so that we can get the last value on the next render called memoizedState. With this property, our FunctionComponent has the same ability to use this.setState as ClaassComponent.

Fiber. MemoizedState is a single necklace watch structure. First, each of our Usestates generates a hook node after it. It will concatenate all the useState corresponding hook nodes of the current component with the next pointer, and the header is Fibre. memoizedState. The purpose of our initialization is to construct and complete it.

To simplify the description, let’s now introduce some variable definitions:

  1. CurrentlyRenderingFiber: Refers to the Fiber object of the currently rendering component, in our case, the corresponding Fiber object of the App

  2. WorkInProgressHook: which hooks are currently running. We can have multiple hooks inside a component and only one hook is currently running.

  3. Hook node: Each useState statement, when initialized, generates an object to record its state, which we call a hook node.

First apply colours to a drawing App, also known as mount stage, we haven’t perform any a useState function, currentlyRenderingFiber. MemoizedState will store the current state of the components, at this time is empty. WorkInProgressHook is also empty.

Next, we are going to execute our first hook statement. The code for executing the first hook statement is relatively simple, which is to create a hook node according to the initial value of useState. Then currentlyRenderingFiber. MemoizedState and workInProgressHook pointing to it.

var hook = {
    memoizedState: null.baseState: null.baseQueue: null.queue: null.next: null
};

if (typeof initialState === 'function') {
    initialState = initialState();  
    InitialState is the first parameter to useState
}

hook.memoizedState = hook.baseState = initialState;

currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
Copy the code

After running the above code, the initial value of our first useState statement can be logged in currentRenderingFiber.

That is, if we are executing the first useState statement:

const [count1, setCount1] = useState(0)
Copy the code

Now the memoizedState of the hook node corresponding to the useState is changed from null to 0. This value will also be returned later as count1, the first entry in the useState return value array.

So what is the second item that we return, setCount1?

This is a function that binds currentlyRenderingFiber and the queue property of the current hook node to the current function context:

  var dispatch = queue.dispatch = dispatchAction
      .bind(null, currentlyRenderingFiber, queue);
Copy the code

DispatchAction is a function like this, which will post updates later, but we can leave the internal logic behind:

function dispatchAction(fiber, queue, action) {... }Copy the code

We bind the first two arguments to it. Do you have any questions about why bind?

In the initial rendering of the App component, we were at the stage of constructing the Fiber tree corresponding to the entire application. At this time, the Fiber nodes of each component would be constructed one by one. We create a Fiber object for a component before executing its corresponding FunctionComponent or ClassComponent’s Render method. At this point, we already know the value of currentlyRenderingFiber beforehand.

However, when the update is triggered later, it is different. We may trigger the update on a component at any time. If the binding is not done here, we cannot determine which node in the Fiber tree triggered the update operation, and we cannot add the update task to the corresponding node.

If you’re a little confused right now, we’ll see how to use it in the update phase.

Next we execute the second Hook statement:

const [count1, setCount1] = useState(0)
Copy the code

Once you understand the above logic, it is relatively easy to understand the second one. After initializing the first one, all follow the same logic:

  1. Create a hook node based on the initialState of useState
  2. Place the newly created node inworkInProgressHookAfter the node pointing to
  3. letworkInProgressHookIt points to the next node, the last node

As long as there are according to this idea, we will generate a list, the list of the head pointer is currentlyRenderingFiber memoizedState, each node is the rest of the corresponding hook useState node.

The following code is the logic we talked about above:

function mountWorkInProgressHook() {
  var hook = {
    memoizedState: null.baseState: null.baseQueue: null.queue: null.next: null
  };

  if (workInProgressHook === null) {
    // When initializing the first hook node
    currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
  } else {
    // Not the first node, put it directly after
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}
Copy the code

Here is our code to initialize the hook return value.

function mountState(initialState) {
  // Initialize the current hook node according to the initial value
  var hook = mountWorkInProgressHook(); 

  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }

  hook.memoizedState = hook.baseState = initialState;
  // Initialize the update queue for the current hook object
  // Later update phase operations will put values in it
  var queue = hook.queue = { 
    pending: null.interleaved: null.lanes: NoLanes,
    dispatch: null.lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
  };
  var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
  return [hook.memoizedState, dispatch];
}
Copy the code

At the end of the execution, the various states are initialized, and the App component’s return statement returns a JSX node. All that’s left is for the React scheduler to finally display the updated results on the page. We’re definitely not going to do that, or we’ll have to write several more chapters.

Next is the update process. That’s calling setCount.

In fact, the general logic is the same as when we first render, but we only initialize the queue on each hook node during initialization, and update the queue will add tasks; In addition, we no longer assign the initial value according to the initialState, but assign the initial value according to the linked list of hook nodes generated last time, and calculate the final result according to the update task on hook node queue.

Here is an example of how we can update a node:

function App() {
  const [count, setCount] = useState(0)
  const [count2, setCount2] = useState(10)

  function handleClick() {
    setCount((count) = > {
      return count + 1;
    })

    setCount2((count2) = > {
      return count2 + 10; })}return (
    <button type="button" onClick={handleClick}>
    count is: {count}
    </button>)}Copy the code

When we click the Button, the update process is triggered.

First, we need to add the update task to the corresponding hook queue.

Each hook corresponds to a queue. Here is the data structure of our queue:

 var queue = { 
    pending: null.interleaved: null.lanes: NoLanes,
    dispatch: null.lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState
};
Copy the code

In the mount phase, we have specified two parameters for setCount: its corresponding Fiber object and its update queue. The entry to our setCount function will be passed in as the third argument, action.

When the first line is executed:

setCount((count) = > {
  return count + 1;
})
Copy the code

We will update the node based on an input parameter:

var update = {
    lane: lane,
    action: action, // This is the input parameter to setCount
    eagerReducer: null.eagerState: null.next: null
};
Copy the code

Queue.pending stores updates that are generated. Its data structure is a single circular linked list. When there is only one node, it points to itself, and when there are multiple nodes, it is inserted into it.

Ps: The circular list is the object to use directly here, but it can also be done with arrays. If you are interested, you can refer to my old article on double-enqueued queues.

var pending = queue.pending;

if (pending === null) {
  // This is the first update. Create a circular list.
  update.next = update;
} else {
  update.next = pending.next;
  pending.next = update;
}

queue.pending = update;
Copy the code

After the procedure above, the queue of the first hook has been processed. Since the React event handler has its own AutoBatching feature, we will not directly calculate the queue. Execute the following hook statement to process the second hook node and queue it. The second processing step is the same as the first, so we omit it.

After the above processing, both hook nodes have only one queue.pending node. You may ask, why is queue.pending structured as a list?

Because the scenario we describe here is quite special, if we perform the same hook operation many times, it will result in multiple nodes, such as:

setCount(1);
setCount(2);
Copy the code

Ps: For those of you who are watching this, please note that the following is not recommended:

setCount(count + 1)
setCount(count + 2)
Copy the code

This should be written as a callback:

setCount((count) = > count + 1)
setCount((count) = > count + 2)
Copy the code

The two forms actually enter a queue differently. I’ll explore the reasons for this in my next blog post. But I don’t think you’re going to be able to see this, because I haven’t read much of this, so you’re going to look bored.

In this case, we append directly to the list of the current queue. In addition, some update tasks may be temporarily suspended due to lack of priority, so the queue may still store information that was not updated last time. If you want to update it again, you need to merge the last unfinished update and then update it.

Next comes the real Update phase. As we mentioned above, this process is very similar to the initialization phase. We’ll just share with you the different points.

We are still regenerating the hooks, only now our memoizedState and Queue are not null, and we are still connecting the hooks that we generated. It is still a single necklace watch structure.

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

if (workInProgressHook === null) {
      // No hook nodes have been initialized yet
      currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
} else {
      // Add to the end of the list
      workInProgressHook = workInProgressHook.next = newHook;
}
Copy the code

Once a new hook node is generated, all we need to do is figure out the value of the new memoizedState based on hook.queue.

var first = baseQueue.next;
var newState = current.baseState;
var update = first;

do {
  var action = update.action;
  newState = reducer(newState, action);

  update = update.next;
} while(update ! = =null&& update ! == first); hook.memoizedState = newState;Copy the code

Finally memoizedState is returned as the first item of the useState return value as before. Next comes the scheduling task. It’s not the scope of useState.

After the above process, we have gone through the initial render and update phase useState. The initial rendering part of the code is basically unchanged, but I’ve skipped over some of the code in the update phase for the sake of clarity.

The above is the source code interpretation of useState, I do not know if my explanation makes you feel clear, if you do not understand or think I say something wrong, you can leave a message in the comment section, I will reply as soon as I see.

Let me tell you my feelings. Personally, I think it should be the most tired article I have written recently.

Still, I hope you enjoyed it.