How do I handle checkbox’s onChange in React?

Im begginer in React programming, and need some help. I got simple component, that uses useState and useEffect hooks. This components is getting a number (as props), how much checkboxes should it render. It looks like this.

Why does handleCheckbox method returns an empty array? Thanks for any ideas

Answer

okay, I spotted quite a few issues which are interlinked (thats okay, you’re a beginner) so I am going to try untangle this with words.. the code is easy to untangle.

Check out this sandbox

the crux of the issue ? Stale closures and async nature of setState. So many people fall victim to this, and I have answered this question quite a few times in different ways. so don’t stress.

React relies on rendering to ensure the latest state is contained with a functions closure. Think about a closure as a “snapshot” of variables at a specific point in time (or better yet, for a specific render cycle).

the useEffect in Component has an empty dependency array. This means, in short, that it will only ever call the code within the useEffect once, when the component first mounts.

useEffect(() => {
   //this code will run only once when the component mounts for the first time
   //index has no bearing since index is a value type which never changes in your example
   //useEffects always react to a change in value of a value type (number, boolean, etc) or if a reference changes of a reference type (spreading an object creates a new reference, for example)
},[])

Now back to stale closures: What the above useEffect does in your example, is create a “snapshot” of the values used in the handleCheckbockChange function, as they are on the first render when passed into the callback methods for onChange

see below here

  const handleCheckbockChange = (e, value) => {
    const { checked } = e.target;

    const states = [...buttonsState]; 

    const indexOfCheckBox = states.findIndex((el) => el.value === value);

    if (indexOfCheckBox !== -1) {
      states[indexOfCheckBox].visible = checked;
    }

    setButtonsState(states);
    console.log(states);
  };

now, remember what I said about “closures” being snapshots? when buttonState is created, it was initialized to an empty array

const [buttonsState, setButtonsState] = useState([]);

so on the first render, your function actually looks something like this in memory

  const handleCheckbockChange = (e, value) => {
    const { checked } = e.target;

    const states = [...[]]; //buttonStates is an emptyArray when this was created!

    const indexOfCheckBox = [].findIndex((el) => el.value === value);

    if (indexOfCheckBox !== -1) {
      [][indexOfCheckBox].visible = checked;
    }

    setButtonsState([]);
    console.log([]); //<-- EMPTY!!
  };

but you may now wonder how on earth this is possible if you are calling setButtonsState in the useEffect?

Well.. now we come to the async nature of setState.

setState batches the updates and only applies them on the next render, hence asyncronous.

Your states variable is initalised in the useEffect, but only after a closure around handleCheckbockChange was already created with your initial value of []. Therefore, handleCheckbockChange never gets this value since by that point the closure assigned to your onChange callback was already created with the initial value.

all your click actions therefore call the very first snapshot of handleCheckbockChange which ever gets the updated value of the buttonsState, because the useEffect runs only once.

Solution

Check out this sandbox.. here again for reference

There are actually a few things you can do here, but the easiest way to solve your situation is to remove the creation of your components out of the useEffect, so they freely rerender getting the latest closure snapshot of handleCheckbockChange in their onChange handlers, everytime a change to buttonsState occurs.