54

React Hooks documentation says to not call Hooks inside loops, conditions, or nested functions.

I understood that the order of execution was important so React can know which state corresponds to which useState call. Given that, it's obvious that a hook cannot be called inside a condition.

But I don't see what is the problem if we call useState inside a loop where the number of iterations doen't change over the time. Here is an example :

const App = () => {
  const inputs = [];

  for(let i = 0; i < 10; i++) {
    inputs[i] = useState('name' + i);
  }

  return inputs.map(([value, setValue], index) => (
    <div key={index}> 
      <input value={value} onChange={e => setValue(e.target.value)} />
    </div>
  ));
}

export default App;

Is there a problem with this code above ? And what is the problem of calling useState inside a nested function, if this function is called on every render ?

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
Olivier Boissé
  • 15,834
  • 6
  • 38
  • 56

1 Answers1

43

The reference states the actual cause,

By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.

and provides the example that shows why this is important.

Loops, conditions and nested functions are common places where the order in which hooks are executed may be disturbed. If a developer is sure that a loop, etc. is justified and guarantees the order then there's no problem.

In fact, a loop would be considered valid custom hook if it was extracted to a function, a linter rule can be disabled where needed (a demo):

// eslint-disable-next-line react-hooks/rules-of-hooks
const useInputs = n => [...Array(n)].map((_, i) => useState('name' + i));

The example above won't cause problems but a loop isn't necessarily justified; it can be a single array state:

const App = () => {
  const [inputs, setInputs] = useState(Array(10).fill(''));
  const setInput = (i, v) => {
    setInputs(Object.assign([...inputs], { [i]: v }));
  };

  return inputs.map((v, i) => (
    <div key={i}> 
      <input value={v} onChange={e => setInput(i, e.target.value)} />
    </div>
  ));
}
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 20
    so the sentence **Don’t call Hooks inside loops, conditions, or nested functions.** is wrong. If the order is respected, we can call hooks inside loops – Olivier Boissé Dec 23 '18 at 20:30
  • 1
    That's correct. It's ok as long as `i < 10` condition leaves no space for errors. – Estus Flask Dec 23 '18 at 20:33
  • 1
    There were mismatched parentheses. It works in other respects, https://stackblitz.com/edit/react-d1atmc – Estus Flask Dec 23 '18 at 20:36
  • there is a warning in the console, because the input switch from uncontrolled to controlled. I found weird to call Object.assign with an array, but its totally correct – Olivier Boissé Dec 23 '18 at 20:38
  • 1
    As for Object.assign, it's perfectly ok with arrays. It's a one-liner for immutable state that could be written as `newInputs = [...inputs]; newInputs[i] = v; setInputs(newInputs)`. I provided a fix for uncontrolled inputs. – Estus Flask Dec 23 '18 at 20:41
  • @UAvalos There's linter error exactly because of the question under discussion. You can disable linter rule per line or permanently if you know what you're doing. – Estus Flask Jul 31 '19 at 16:10
  • @EstusFlask even when you disable the linter rule or go around it by using useCallback, the app won't run! – U Avalos Aug 08 '19 at 22:03
  • 1
    @UAvalos What does 'not run' mean? The answer contains workable code. If your case differs, consider asking a question and providing https://stackoverflow.com/help/mcve that can replicate your problem. – Estus Flask Aug 09 '19 at 06:07
  • 1
    Does not run means it gives a React runtime error saying that the code violates a react hook rule. The code is almost identical to this case :-) – U Avalos Aug 09 '19 at 14:33
  • have you actually tried running your code @EstusFlask? jsfiddle example perhaps? – U Avalos Aug 09 '19 at 14:33
  • @UAvalos Of course. I always do this in case I have doubts regarding workability. In this case I have none. See https://stackoverflow.com/questions/53906843/why-cant-react-hooks-be-called-inside-loops-or-nested-function/53906907?noredirect=1#comment94658436_53906907 . Custom hook is also working, https://codesandbox.io/s/vigorous-greider-5couy . That you have an error means that your case differs from the answer in some way, that's why I suggested to ask a question. – Estus Flask Aug 09 '19 at 14:49
  • you're right @EstusFlask. It works in this fiddle: https://jsfiddle.net/nvrpc0oa/ . When i get back to work i'll see if i can make it look more like the production code that was failing. Maybe we need to upgrade react? – U Avalos Aug 10 '19 at 15:48
  • @UAvalos If it's runtime error there are not so many reasons for it to happen. It means that a hook was called outside component lifecycle, it should be reproducible with built-in hook like useState, too. This could happen if you import a hook from one `react` copy and render it with another one. – Estus Flask Aug 10 '19 at 16:55
  • I was able to repo: https://jsfiddle.net/6fagznLs/2/ If you notice, we have an initial calls that tells how many times the for-loop should run. I'll open a new question: https://stackoverflow.com/questions/57451412/run-react-hook-inside-variable-for-loop – U Avalos Aug 11 '19 at 15:41
  • 1
    @UAvalos I see. This is very far from what's discussed in this question. The answer in your question is correct, you can't do that. You likely have XY problem that needs to be solved in some way that doesn't require hooks inside a loop. I cannot recommend how exactly it should be refactored. Possibly that's a bounty question that needs to be rewritten from XY to 'what I want to achieve and this is my current attempt'. – Estus Flask Aug 11 '19 at 20:22