1

The following React component is rendered every 50ms. The value of s increases by 1 every second. When it does, I'd like to add the value of word to an array in state.

When the code runs, the word gets added to the array as intended. Then React does its second Strict Mode render of the entire app end everything looks the same.

On next iterations however, when s increases, the second pass only results in a different behavior and nothing gets added to the array beyond that.

export const Component = ({ word, s }) => {
    const lastS = useRef()
    const [words, setWords] = useState()
    
    if (s !== lastS.current) {
        console.log('adding', word, words, s, lastS.current)
        setWords([...words, word])
        lastS.current = s
    }
    console.log('rendered', word, words, s, lastS.current)
}

Console output when s changes for the first time is what I expect:

1st pass : adding apple > Array[] 9 undefined   // inside "if" statement
1st pass : rendered apple > Array[] 9 9         // component renders
1st pass : rendered apple > Array["apple"] 9 9  // state changed, rerender

2st pass : adding apple > Array[] 9 undefined   // same as 1st
2st pass : rendered apple > Array[] 9 9         // same as 1st
2st pass : rendered apple > Array["apple"] 9 9  // same as 1st

However, the next time s changes I get the following output. The second pass doesn't add word to the array.

1st pass : adding orange > Array["apple"] 10 9
1st pass : rendered orange > Array["apple] 10 10
1st pass : rendered orange > Array["apple", "orange"] 10 10

2st pass : rendered orange > Array["apple"] 10 10 <------------ why?

Why are the two results different? What is the side effect here?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Cirrocumulus
  • 520
  • 3
  • 15
  • 1
    TL;DR - `setWords((prev) => [...prev, word])` – Phil May 31 '23 at 06:19
  • I may be wrong, but I don't think the result should be any different when doing this, and after trying it I can confirm that the output is strictly identical. – Cirrocumulus May 31 '23 at 06:27
  • Indeed, fixed both typos, thank you. – Cirrocumulus May 31 '23 at 06:36
  • Think I've managed to reproduce what you're seeing here ~ https://codesandbox.io/s/hopeful-bird-0f8y7s?file=/src/App.js – Phil May 31 '23 at 07:47
  • 1
    By design apparently ~ https://github.com/facebook/react/issues/20835 – Phil May 31 '23 at 07:56
  • Thank you very much. I find it hard to wrap my head around this, and I've been staring at your sandbox for 20 minutes. Does this mean that if Strict Mode is not active then the array will grow at each iteration? – Cirrocumulus May 31 '23 at 08:25
  • That's right but as far as React is concerned, your code is bad and you should feel bad (I joke). In general, you should not mutate state during render – Phil May 31 '23 at 08:27
  • I understand (I joke, because I'm only starting to). Should I answer my own question and refer to your sandbox and GitHub issue or would you like to do it? – Cirrocumulus May 31 '23 at 08:43
  • I had no intention of writing up an answer so go right ahead – Phil May 31 '23 at 08:48
  • I'll do that as soon as I figure out *exactly* what mutates in your sandbox. I understand that "mutation during render is specifically something React does not support", but I'm still trying to follow the sequence of mutations happening here and the precise reasons for the behavior I got. In the meantime I promise never to set state in render again. – Cirrocumulus May 31 '23 at 09:09
  • You're mutating the `words` state as part of the render, it does not matter if you've gated it with a ref check. Ideally, you'd not use a ref and add an effect hook to update `words` when `s` changes – Phil May 31 '23 at 09:11
  • Yes, this is what I ended up doing as a matter of fact. But it's that part that says "it does not matter if you've gated it with a ref check" that puzzles me. As I understand it, refs are kept across renders and are mutable, and after years of writing "if" statements I don't see how a comparison with a maintained value can't matter, unless I misunderstand the concept of maintaining a reference to a value. Perhaps a ref *seems* to behave like a regular variable but in fact does something else entirely. – Cirrocumulus May 31 '23 at 09:49
  • It's not the ref that's the problem, it's the fact that you called a state setter (`setWords`) during render. Usually that's a good way to end up in an infinite render loop. You were only saved from that by the `if` – Phil May 31 '23 at 10:14

0 Answers0