A colleague brought me a fun game of whack-a-mole buggy set of checkboxes made with React. Interacting with any checkbox was also unchecking (or checking) unrelated checkboxes in the collection! It seemed to do so at random. ????
Proving (once again) that someone else’s problems are always more compelling to work on than my own, I workshopped it a bit until I had a repro case:

The problem ended up being in the way that React state was being updated. Here’s how I set it up –
state variables
const allCars = ['toyota', 'subaru', 'ford', 'honda', 'hot wheels'];
const [cars, setCars] = useState(['toyota']);
generating the checkboxes by mapping through allCars:
return (
<div className="p20 pt2">
<p>Intermittent checkboxes in React</p>
{_.map(allCars, (car: any) => (
<div className="height-40">
<Checkbox
id={car}
value={car}
onChange={() => handleSelectCar(car)}
isChecked={_.includes(cars, car)}
label={car}
classes="check-input"
/>
</div>
))}
</div>
);the method that updates React state:
// BAD CODE!!! DON'T USE!
const handleSelectCar = (car: string) => {
const carsCopy = [...cars];
if (cars.includes(car)) {
// If the car is already in the list, remove it
setCars(carsCopy.filter((c) => c !== car)); }
else {
// If the car is not in the list, add it
setCars([...carsCopy, car]); }
};In this original version of the handler, I thought making a copy of “cars”, altering it, and setting it to the state variable would prevent these kinds of issues. I wasn’t mutating the original, I was using state variables, what gives?
After scratching my head about it for a while and reviewing what the React docs had to say about recycling the previous state, I wondered if I needed to be setting the state via an updater function.
// GOOD CODE! Or at least better...
const handleSelectCar = (car: string) => {
setCars((prevCars) => {
if (prevCars.includes(car)) {
// If the car is already in the list, remove it
return prevCars.filter((c) => c !== car);
} else {
// If the car is not in the list, add it
return [...prevCars, car];
}
});
};And that fixed it:

I think I went wrong in a couple places here:
- Using the spread operator (
[...]) creates a shallow copy of an array, not a deep copy. (However, simply replacing […cars]; with const carsCopy = _.cloneDeep(cars); was not enough to salvage my original non-working version) - https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state Since I was taking previous state into account, I needed to use an updater function