Why Call React Hooks at the Top Level?

Viewed 8083

I was reading the React-hooks concepts. I came through a rule which says Don't call React hooks inside conditions. Here they provided the explanation link.

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

I understood what they want to say, but I can not get the exact reason, like why I can't use useEffect in if-else block?

There is one more statement

So how does React know which state corresponds to which useState call?

The useState is different call every time and it can return new "[state, setState]" each time, so what is difficult here to know who called which useState?

3 Answers

Bassically, hook rely on a call index. React doesn't know what a given useState() returned as it's state the previous render, but it does know that the first call to useState() by that component returned a [1,2] as it's value, and the second call returned false. Now, if the only thing react knows is what was the given return for a given call index, what do you think could happen if i could write a componente like this:

const [a, setA] = React.useState([1,2,3]);
let c;
if(a === [3,2,1]){
  c = React.useState('X');
}
const [b, setB] = React.useState(false);
React.useEffect(() => setA([3,2,1]), []);

now, react knows from the first render that the first call returns [1,2,3] and the second false. then the effect rerenders the component, now it's not the first render so the first call will return the state [3,2,1] since it was updated, the second call (the one c = ...) will return false, but then react sees a third call, what should it return?

From react's point of view, this makes no sense, from your point of view, this can lead to an enormous amout of bugs and problems.

Of course, neither my very basic explanation nor React's are a lot, that's why a come bearing sources, Dan Abramov, one of the people working in react has a very long and detailed post on his blog about this, you can find it here. He also posts a lot of other stuff about how react works behind the curtains, it's worth the read.

It is not about who called which hook useXXXX(i.e useState, useEffect, etc). It is about how hooks are internally implemented and associated with each component. There are a lot of other problems which to solve React relies on the call order of the hooks.

From the docs Hooks FAQ section

How does React associate Hook calls with components?

There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state.

Internally hooks are implemented like a queue where each hook represents a node having reference to the next one. The internal structure might look something similar to this,

{
  memoizedState: 'a',
  next: {
    memoizedState: 'b',
    next: null
  }
}

Take the example of having 4 state variables by calling useState 4 times. With each hook call if the value has not been initialized(i.e on first render) it will initialize the value else read from the memory cell then moving to the next hook internally.

// 4 independent local state variables with their own "memory cell"
// nothing is called conditionally so the call order remains the same
// across renders
useState(1)   // 1st call 
useState(2)   // 2nd call
useState(3)   // 3rd call
useState(4)   // 4th call
useState(1)

if (condition) {   // if condition false hook call will be skipped
  useState(2)   
}

useState(3)   
useState(4)   

Now when you call a hook conditionally if the condition is false the hook call will be skipped. This means every subsequent hook call will shift by 1 in the call order resulting in failure to read state value or replacing an effect or many more hard to detect bugs.

So in general it is a bad idea to call any hook conditionally. Only call the hook in top-level(not inside condition, nested functions, or loops) which will help React to preserve the state of hooks for multiple hook calls.

From this answer of React document, it mentioned that hooks are store in a "memory cells" and render in order (" moves the pointer to the next one")

There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state.

Which is match with below section of the link you provided which has some more explanation

// First render // ------------

useState('Mary') // 1. Initialize the name state variable with 'Mary'

useEffect(persistForm) // 2. Add an effect for persisting the form

useState('Poppins') // 3. Initialize the surname state variable with 'Poppins'

useEffect(updateTitle) // 4. Add an effect for updating the title

// ------------- // Second render // -------------

useState('Mary') // 1. Read the name state variable (argument is ignored)

useEffect(persistForm) // 2. Replace the effect for persisting the form

useState('Poppins') // 3. Read the surname state variable (argument is ignored)

useEffect(updateTitle) // 4. Replace the effect for updating the title

In the second render section, the docs said Read the ... variable means when a useState called in the second time, it doesn't generate new [state, setState], it comes to the "memory cells" to read state value instead and return then we assign it to new array by const [state, setState] = useEffect(). That's why React can guarantee that setState will not be changed each re-render

React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.

Related