2

I have written a simple component to reproduce the problem I am having in a much larger project component.

Say I have an arrayA where I'd like to continuously add the value 1 on every button click and a state arrayB I would like to update on the button click.

export default function UseStateTest(props) {
  const arrayA = [];
  const [arrayB, setArrayB] = useState([1, 2]);

  function handleClick() {
    arrayA.push(1);
    setArrayB([3, 4]);
    console.log(arrayA);
  }

  return <button onClick={handleClick}>Test</button>;
}

With the setArrayB there, the code behaves incorrectly and when I click the button: arrayA will always have 1 element 1 (I think, from experience in my other code where I'm having a similar problem, that the code is actually replacing the element instead of pushing).

When I comment the setArrayB out, the code behaves correctly, with a new element '1' being added to the array on every button click.

Please may someone help me understand what is causing this behaviour.

Tommy Wolfheart
  • 440
  • 4
  • 16

1 Answers1

1

Your component function is called each time your component needs to be re-rendered. You get a different arrayA on each call to your component (as with any other function). That's why there's a useState hook (and some others, like useRef): So you can persist stateful information across renders.

Without the call to setArrayB, your component doesn't change its state, so React doesn't re-render it (doesn't call your function again). With the call to setArrayB, you're changing the component's state, causing a re-render, which does a new call, which creates a new arrayA (and a new handleClick).

You can't store any information that should persist between renders in simple variables/constants within the function, since those are recreated on each function call. Instead, use useState (for state information that, when changed, should cause your component to re-render), useRef (for stateful information that shouldn't cause your component to re-render [rare]), and various other hooks.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • I see. Thank you very much.Yeah changing ```const arrayA = [];``` to ```const [arrayA, setArrayA] = useState([]);``` and then doing ```arrayA.push(1)``` works. Does rendering a component take a significant amount of time though. I am thinking of creating a new, smaller component and then include it in the main one so that only the smaller part is rerendered and I was wondering if it is worth doing. – Tommy Wolfheart Feb 13 '22 at 13:04
  • 1
    @TommyWolfheart - It depends on how much work your component does to re-render. A couple of notes: That `push` call breaks one of the rules of React state, which is "Do not directly modify state." Instead, [do this](https://stackoverflow.com/a/54677026/157247). If you're worried about efficiency, you may want to look at [this](https://stackoverflow.com/a/70293410/157247) as well, which discusses various ways to allow child components to avoid re-rendering when their parents do (and possibly [this](https://stackoverflow.com/a/69272573/157247)). (Disclosure: All are links to answers I wrote.) – T.J. Crowder Feb 13 '22 at 13:50
  • 1
    Thanks so much for the help! I'll have a look at the links you've shared. – Tommy Wolfheart Feb 13 '22 at 14:11
  • Sorry. One more question. Is there a way I can achieve something to the effect of ```[disabled, setDisabled] = useState(!prevDisabled)``` to make the attribute ```disabled``` for a button ```false``` if on the previous rendering it was ```true```. I tried an if statement to set the variable but I'm running into an infinite loop where I declared the state then set it in the if statement and the loop continues. – Tommy Wolfheart Feb 13 '22 at 16:31
  • @TommyWolfheart - So you want `disabled` to be flipped each time your component renders? Beware that React can re-render your component at any time for any reason, so that's going to be a bit...chaotic. If you really want to do that kind of thing where you need to store state information that you *don't* want to cause re-rendering when you change, you can store it in a ref via [`useRef`](https://reactjs.org/docs/hooks-reference.html#useref). Basically: `const disabledRef = useRef(false); const disabled = (disabledRef.current = !disabledRef.current));` But it's probably not a good idea. :-) – T.J. Crowder Feb 13 '22 at 16:35
  • I actually want to do a bit more than flip. I want ```disabled``` to be reevaluated everytime the component renders. I basically have buttons for clothing options: 'small', 'medium', 'red', 'blue' etc. and when one button is clicked from the Options parent component, the OptionValues child components will all rerender but now whether the buttons are disabled or enabled actually depends on an array of available OptionValues given the current selection. So, I just want to check if the OptionValue's id is in the available option values array and then enable the button, if it is, when rerendering. – Tommy Wolfheart Feb 13 '22 at 16:44
  • I guess the useRef approach will probably work fine for that because the available option values array only changes on each button click. Unless if there's an even better solution? – Tommy Wolfheart Feb 13 '22 at 16:51
  • @TommyWolfheart - You wouldn't want a ref for that, just compute `disabled` based on the current state of the component. After your various `useState` calls, `const disabled = /*...calculate from the state information...*/;` If computing the setting is a ***lot*** of work, you can do it only when one of the inputs change by use [`useMemo`](https://reactjs.org/docs/hooks-reference.html#usememo). But it has to be a lot of work for that to be worthwhile. – T.J. Crowder Feb 13 '22 at 16:52
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/241980/discussion-between-tommy-wolfheart-and-t-j-crowder). – Tommy Wolfheart Feb 13 '22 at 16:58