useEffect() cleanup functions not updating state in the expected order

Viewed 15

I'm looking to have child components update a state value through the useContext hook when they are unmounted from the DOM, but am unable to make sense of the order in which their clean up functions (the functions returned from a useEffect hook) are called.

What I'm trying to achieve in the example code is to have the switches all individually able to modify the count of switches in an "on" state so that it's accurate, but also when they are removed from the DOM by the "Remove all switches" button they do the same. It seems like what is happening is that those updates when they are removed from the DOM are not happening in the order I'd expect, and the final state of that counter is not accurate.

If you add several of the switches, turn several of them "on", and then remove all of them, you'll see that the counter in the parent component is not set correctly. I've added a console.log call in the returned function from the useEffect hook, what's especially confusing here to me is that it looks like the counter is being set correctly, but somehow not in the correct order?

(Here is the example code running on StackBlitz.)

import React, { useState, createContext, useContext, useEffect } from 'react';
import './style.css';

const SwitchContext = createContext();

const ChildComponent = () => {
  const [switchOn, setSwitchOn] = useState(false);
  const { numberOfSwitchesOn, setNumberOfSwitchesOn } =
    useContext(SwitchContext);

  useEffect(() => {
    return () => {
      if (switchOn) {
        console.log(
          `Setting number of switches on to: ${numberOfSwitchesOn - 1}`
        );
        setNumberOfSwitchesOn(numberOfSwitchesOn - 1);
      }
    };
  }, [switchOn]);

  const toggleSwitch = () => {
    if (switchOn) {
      // We are turning off this switch...
      setNumberOfSwitchesOn(numberOfSwitchesOn - 1);
    } else {
      // We are turning it on...
      setNumberOfSwitchesOn(numberOfSwitchesOn + 1);
    }

    setSwitchOn(!switchOn);
  };

  return (
    <button onClick={() => toggleSwitch()}>{switchOn ? 'on' : 'off'}</button>
  );
};

export default function App() {
  const [numberOfSwitches, setNumberOfSwitches] = useState(1);
  const [numberOfSwitchesOn, setNumberOfSwitchesOn] = useState(0);

  let switches = [];
  for (let i = 0; i < numberOfSwitches; i++) {
    switches.push(<ChildComponent key={`switch-${i}`} />);
  }

  return (
    <SwitchContext.Provider
      value={{
        numberOfSwitchesOn,
        setNumberOfSwitchesOn,
      }}
    >
      <div>
        <button onClick={() => setNumberOfSwitches(numberOfSwitches + 1)}>
          Add switch
        </button>
        <button onClick={() => setNumberOfSwitches(0)}>
          Remove all switches
        </button>
        <p>Number of switches on: {numberOfSwitchesOn}</p>
        {switches}
      </div>
    </SwitchContext.Provider>
  );
}
1 Answers

This is because they are all updating the same value simultaneously which react does not like. If you want to make sure each ChildComponent is working with the actual latest value you could use functional updates as described in the react docs

useEffect(() => {
    return () => {
      if (switchOn) {
        console.log(
          `Setting number of switches on to: ${numberOfSwitchesOn - 1}`
        );
        setNumberOfSwitchesOn((prev) => prev - 1);
      }
    };
  }, [switchOn]);

Have a look at Your stackblitz sample edited

Related