React Hooks: No magic, just arrays

I’m a fan of the new React feature Hooks. However, there are some seemingly strange restrictions when you use React Hooks. In this article, FOR those of you who are struggling to understand these limitations, I’ve tried to explain why they exist in a series of charts and graphs.

Understand how hooks work

I have heard that many of you are confused by the magical effect of hooks, so I will try to demonstrate how hooks work in a simple way.

The principle of hooks

The React team’s official documentation on how to use hooks highlights two main principles:

  • Do not call hooks in loops, conditional statements, or nested functions

  • Hooks can only be called in the React function component

The second point I think is obvious. In order to add capabilities to function components (such as state, class declaration cycle methods), you of course need a way to assign these capabilities to function components: use hooks.

The first rule, however, is easy to confuse. Just use one API, why are there so many restrictions? This is what I will explain below.

State management in hooks, just manipulating arrays

To understand hooks more clearly, let’s take a look at how to simply implement the hooks API.

Note that the code below is just a demo to give us an idea of how hooks work roughly. This is not a true internal implementation in React.

How do you implement useState?

Let’s take a look at an example of how useState might work internally.

The component code is as follows:

function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi");
  const [lastName, setLastName] = useState("Yardley");
​
  return (
    <Button onClick={() => setFirstName("Fred")}>Fred</Button>
  );
}
Copy the code

What useState implements is that you can return the second element of the array via this hook as a setter method to modify this state.

So how might React implement useState?

Let’s think about how useState would be implemented inside React. In the implementation below, state is stored outside of the render component, is not shared with other components, and can be accessed in the component’s subsequent render in a scoped manner.

1) State initialization

Create two empty arrays for setters and state, and point the pointer to 0:

2) First render of component

When first render the function component.

Each useState call, when first executed, adds a setter function (associated with the corresponding array index) to the setter array; Then add state to the corresponding state array:

3) Component subsequent (not first)render

Each subsequent component’s render resets the pointer to 0, and each call to useState returns the state and setter in the two arrays of the pointer, and then increments the pointer position by 1.

4) Setter call handling

Each setter function is associated with a pointer position. When a setter function is called, we can use the pointer associated with the function to find the corresponding state and modify the value of the corresponding position in the state array:

Finally, take a look at the simple implementation of useState

let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;
​
function createSetter(cursor) {
  return function setterWithCursor(newVal) {
    state[cursor] = newVal;
  };
}
​
// This is the pseudocode for the useState helper
export function useState(initVal) {
  if (firstRun) {
    state.push(initVal);
    setters.push(createSetter(cursor));
    firstRun = false;
  }
​
  const setter = setters[cursor];
  const value = state[cursor];
​
  cursor++;
  return [value, setter];
}
​
// Our component code that uses hooks
function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
  const [lastName, setLastName] = useState("Yardley"); // cursor: 1
​
  return (
    <div>
      <Button onClick={() => setFirstName("Richard")}>Richard</Button>
      <Button onClick={() => setFirstName("Fred")}>Fred</Button>
    </div>
  );
}
​
// This is sort of simulating Reacts rendering cycle
function MyComponent() {
  cursor = 0; // resetting the cursor
  return <RenderFunctionComponent />; // render
}
​
console.log(state); // Pre-render: []
MyComponent();
console.log(state); // First-render: ['Rudi'.'Yardley']
MyComponent();
console.log(state); // Subsequent-render: ['Rudi'.'Yardley']
​
// click the 'Fred' button
​
console.log(state); // After-click: ['Fred'.'Yardley']
Copy the code

Why cannot the order in which hooks are called change?

What happens if we change the order in which hooks are called based on some external variable, or on the state of the component itself?

Let’s show you what goes wrong:

let firstRender = true;
​
function RenderFunctionComponent() {
  let initName;
  
  if(firstRender){
    [initName] = useState("Rudi");
    firstRender = false;
  }
  const [firstName, setFirstName] = useState(initName);
  const [lastName, setLastName] = useState("Yardley");
​
  return (
    <Button onClick={() => setFirstName("Fred")}>Fred</Button>
  );
}
Copy the code

In the code above, our first useState is in a condition branch. Let’s look at the bugs introduced this way.

1) First render

After the first render, our two states, firstName and lastName, correspond to the correct values. Let’s look at what happens when the component is rendered the second time.

2) Render the second time

After the second render, our two states, firstName and lastName, become Rudi. This is clearly wrong and must be avoided using hooks like this! But this also shows why the order in which hooks are called cannot be changed.

The React team explicitly highlights two principles for using hooks, which would cause inconsistencies in our data if we didn’t use them!

Think of the actions of hooks as those of arrays, and you are less likely to violate these principles

OK, now you know why we cannot call hooks in conditional blocks or loops. If you change the order in which hooks are called in render, the Pointers on the array do not match the useState in the component, thus returning the wrong state and setter.

conclusion

I hope I have clarified the general principle of the sequence of hooks calls. Hooks are a nice optimization for the React ecosystem. There is a reason people are excited about hooks. If you treat the actions of hooks like arrays, then you generally do not violate the principle of using hooks.