A revision history

2020/05/31

  • New examples have been added to better explain how Hooks work and why React makes restrictions
  • Revised some concept explanation

Written on the front

This article aims to help you understand some of the concepts in the official documentation by uncovering some of the technical details hidden in React

Readers can see this article as a supplement to the official documentation

I use the method of question-answer, that is, I first ask the question “Why?” according to the rules of use provided by the official documents. Then we will answer the Why according to the actual debugging, and finally we will systematically sort out the Why into How. If you have a better way of writing, you are welcome to leave a comment and discuss

In addition to the reading experience, I will not paste too much source code, to avoid interrupting your readers.

The body of the

See the details behind some of the limitations of Hooks

Why Hooks should only be written in FCComponent? How did React know?

React initials. ReactCurrentDispatcher is a global variable that keeps track of the current dispatcher

Dispatcher can be understood as an agent for Hooks

Because you are executing Hooks outside of FCC, React is either not initialized or Hooks are not associated with ReactCurrentDispatcher, most scenarios are reported because of life cycle mismatches, React does not know 100% if your Hooks are correctly timed

React useState how to determine which State to read or write using the initial value without a Key?

The official documentation is vague on the connection between Hooks execution order and State reads and writes

How does React know which state corresponds to which useState? React depends on the sequence of Hook calls.

On this important detail, it must be said, the official answer is ambiguous.

React uses a similar State cell concept for State reading and writing. By separating State and setState into two arrays, you can determine which State to read and write using the array subscript. But React’s Fiber-based architecture wasn’t that simple

React stores State. What is a Hook

React’s handling of states is not complicated. It is similar to the following linked list structure to store an internal FCC State declared via useState

{   
    memoizedState:1
    next: {
        memoizedState:"ff"
        next: null}}Copy the code

React can conveniently read and write states sequentially through the next pointer

Again, front-end developers are recommended to have a basic knowledge of data structures, which will help you understand the code better

The Hook? React how do you store a Hook?

A Hook is an object, and of course everything in JS is an object. React declares a Hook as such a structure

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

Just like State, Hook is a one-way linked list structure, and memoizedState here is the same as the one above, well if you follow the rules, it’s the same……

The official website does not explicitly define Hook, Hook mainly has a queue property compared to State, so what is this?

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

This is the React queue structure declaration. Without going into Fiber’s details about how queues are used, we’ll just make a guess. Queue stands for queue, pending might mean an executing dispatch, LastRenderedReducer, here is a default function. In the update stage, the Reducer function used last time to update the State is saved. As for lastRenderedState, it is naturally the previous State.

In combination with the queue structure, we can try to define a Hook. A Hook is a manager that manages the State logic handler functions effectively through a queue

Considering that Hook is not only useState useEffect, but also React source code is constantly changing, the definition here may not be precise. However, this series of articles is not a one-off article. With further details and discussion, I will update some relevant definitions and contents to revise the original version to strive for rigor and consistency

The concepts here are close to Redux, but before getting into the details, this article will focus on the Hooks rules. I’ll discuss the State update management mechanism inside React and how it relates to Fiber in a future article.

After learning how React stores State and Hook, and having a clear structure definition of Hook, add a Fiber rendering logic, that is, in the commit stage, once rendering takes place, all rendering will be completed without local rendering. It’s a complete “all nodes” each time.

All nodes are quoted here, but it’s still too expensive to traverse Fiber’s tree using a linked list, so React has been optimized for this section in this article

In this case the FCC ReRender causes all the internal Hooks to be executed, so we’ll change the example from the official website slightly and explain it later

Example 1.0

"use strict";
function Counter({ initialCount }) {
    const [count, setCount] = React.useState(1);

    if (count === 1) {
        const [count2, setCount2] = React.useState(2);
    } 

    const [surname, setSurname] = React.useState('Poppins');

    return /*#__PURE__*/React.createElement(React.Fragment, null."Count: ", count, /*#__PURE__*/React.createElement("button", {
        onClick: (a)= > setCount(initialCount)
    }, "Reset"), /*#__PURE__*/React.createElement("button", {
        onClick: (a)= > setCount(prevCount= > prevCount - 1)},"-"), /*#__PURE__*/React.createElement("button", {
        onClick: (a)= > setCount(prevCount= > prevCount + 1)},"+"));
}


ReactDOM.render(React.createElement(Counter, { initialCount: 1 }, null),
    document.getElementById('root'));Copy the code

For debugging purposes, I use only the two libraries necessary for React, and the code in the example does not use JSX

Before going into specific examples, sort through the above and some background information

To understand that FCC ReRender causes all Hooks to be reexecuted, there are two phases for State “mount” and “update”, both phases are triggered by different dispatchers, It also calls functions like mountState and updateState, respectively. The path forks before Hooks are executed. React is called renderWithHooks. React checks if there is memoizedState on the current node. If there is no memoizedState on the current node, mount it and update it

The current node is declared in FormUnitofWork and passed to renderWithHooks via beginWork. UnitOfWork is a FiberNode, As it relates to the working logic of the Fiber architecture, we have a concept that we will discuss in detail in a future article

Summary:

  • Hooks are repeated with FCC ReRender
  • Hooks and states are kept in a one-way list with memoizedState the same as in the State one-way list
  • Read and write State Two paths exist: mount and update
  • Each FCC has its own State table stored

Going back to the example above, after the first render, the two lists of State and Hooks on the Counter node should be

// State List
{   
    memoizedState:1.next: {
        memoizedState: 2.next: {
            memoizedState: "Poppins".next: null}}}// Hooks List
{
    memoizedState: 1.queue: {
        dispatch: fn()
    },
    next: {
        memoizedState: 2.queue: {dispatch: fn()
        },
        next: {
            memoizedState: "Poppins".queue: {
                dispatch: fn()
            }
            next: null}}};Copy the code

Here the structure is simplified and some attributes are removed to make it easier to understand

Then ReRender is triggered by clicking setCount + 1. The second useState will not be ReRender because count = 2. React doesn’t know this and will assume that you are following the rules. This leads to an interesting result

const [surname, setSurname] = React.useState('Poppins');
Copy the code

We expected the surname to be ‘Poppins’ because we didn’t make any changes, but React now returns an array of 2. Because in the update path, the second memoizedState of the Hook is 2, not ‘Poppins’, React follows the pointer of the Hook in order and calls the dispatch in the queue, it doesn’t care about your real logic, However, the early execution mentioned on the official website is actually ambiguous. Everything is in order with React. There is no pre-execution of post-installed hooks, but what you expect does not correspond with what it actually does. This can be illustrated by a graph

The second time React gives you what you want1                1
2                'Poppins'
'Poppins'

Copy the code

React gives you a result that you think is wrong, but it thinks is right. It’s a little counterintuitive, I have to say, and a little anti-human. React probably knows it’s not a good idea to do this, so it’s defensive. React throws exceptions for two situations

  • Hooks fail to match names in the same order twice
  • The number of Hooks executed is inconsistent between the two times. This also gives an error because only two Hooks were actually executed in the second rendering. React keeps track of the Hooks execution because it saves the List from the last one, so it compares

How to track React? This involves the design of workInProgress in Fiber

Hooks conflict with variables

In this section, let’s look at a slightly more complex example that fits the actual requirements

Example 1.1

"use strict";
function Counter({ initialCount }) {
    let setDocumentTitle = null;
    let documentTitle = 'unknown';
    const [count, setCount] = React.useState(1);

    if (count === 1) {
        const [title, setTitle] = React.useState('Poppins');
        documentTitle = title
        setDocumentTitle = setTitle
    } else {
        const [title2, setTitle2] = React.useState('Jacky');
        documentTitle = title2
        setDocumentTitle = setTitle2
    }
    React.useEffect((a)= > {
        document.title = documentTitle
    })


    return React.createElement(
        React.Fragment, null."Count: ", count,
        React.createElement("button", {
            onClick: (a)= > setCount(initialCount)
        }, "Reset"),
        React.createElement("button", {
            onClick: (a)= > setCount(prevCount= > prevCount - 1)},"-"),
        React.createElement("button", {
            onClick: (a)= > setCount(prevCount= > prevCount + 1)},"+"),
        React.createElement("button", {
            onClick: (a)= > setDocumentTitle(prevTitle= > prevTitle + count)
        }, "setTitle")); } ReactDOM.render(React.createElement(Counter, {initialCount: 1 }, null),
    document.getElementById('root'));Copy the code

Just to give you a quick explanation of this example, there are two buttons, and I want the setTitle button to do two different things by setting the count button, giving the document a different Title, and to do that, I’m using two variables, React cannot detect condition as long as the Hooks number is the same and the name is the same. I may feel smitten because I cannot detect condition as long as the Hooks number is the same. Sure enough, we didn’t get an error, but with example 1, we should know that it’s not that simple.

Originally wanted to get a GIF results to charge, if anyone has a good video to GIF tool, ask to inform

In this case, count is 2, according to the expected logic, click setTitle should result in ‘Jacky2’, but the screenshot shows Poppins2, React also does not have any error, indicating that we did bypass the detection, but the result is not what we expected, why?

Example 1.0 already shows the answer, and Example 1.1 better validates some of the above conclusions

  • Hooks only initialize State during the mount phase, which is determined by the State of the component, so Hooks are ReRun all the time, but the lifecycle is different
  • In The Mount phase, the Hooks list is mapped to the State list by memoizedState, but once in the Update phase, Hooks do not check that the mapping is correct, so the order of mapping established in the Mount phase is a guarantee of correct reads and writes

How does React know which state corresponds to which useState? React depends on the sequence of Hook calls.

We analyze the execution of example 1.1 based on these two conclusions

The first time and the second time1                2
'Poppins'        'Poppins'
Copy the code

If you use Hooks the first time, you must know that JavaScript is broken and you did not enter if. So the counterintuitive implementation of Hooks is fully illustrated in Example 1.1

So why isn’t the documentation on the official website explaining the important detail that Hooks actually have 2 lifecycles? The absence of this crucial detail leads to difficulty in understanding. I think that on the one hand, Hooks are designed to eliminate the need to worry about component lifecycles. The ClassComponent lifecycles carry a certain mental burden. Now you can understand why Hooks must not use the various common programming methods such as condition, loop, etc., in order to keep the mount and update phases consistent, but is this really a reasonable design?

But I’m sure there will be bugs, as in the example above. There will always be ways to bypass detection. In complex applications, it is difficult to ensure that the written code conforms to the official rules. And I’m sure most programmers don’t have enough time, especially after work, to spend a few days carefully debugging and thinking about this part of the code to understand the rationale behind it and guess the author’s design intent, which is why I’m writing this article.

Ideally, the React team can find a good solution to unify the implementation of the two life cycles, so as to smooth out the inherent differences. Otherwise, the cost of the poor implementation may be borne by the majority of developers. From the perspective of the React team, we should start with the documentation and fully explain the details. Even if the implementation of these details doesn’t look so elegant, that shouldn’t hide behind a good design.

Feynman said, I can’t really understand what we can’t rebuild, so for the next fix, I’ll take the Hooks implementation out of React, let’s rebuild it and see how the system relates to Fiber, what are the hidden “bad” implementations? If you’re interested, please follow me and give me a thumbs up or share. I wish more React developers knew about this detail, which would help them avoid working in a pit, and maybe save a 996 game. 😀

Is written on the back

This section is expected to be refined through continuous revision and will eventually become a query manual for React technical details. It is written in this way based on some of my experience with evolution in previous architecture work. If you are interested in the daily life of a front-end architect, you can also write a separate article.