State updates dependent on previous state

One of the most common mistakes I see day to day is how the react useState hook gets used.

A common use case is updating state based on previous state. In practise the mistake looks like the below code, where a developer will update their state using the state captured in MyComponent's scope.

Incorrect usage

// Wrong
const MyComponent = () => {
  const [count, setCount] = useState(0);

  return (
    <>
      Count: {count}
      <button onClick={() => setCount(count + 1)}>
        Increment count
      </button>
    </>
  );
};

While this does tend to work most of the time, as soon as you start making the behaviour more complex where you make multiple updates to state in one react update cycle, it will break.

The problem is that setCount is actually asynchronous. It queues a state update, rather than mutating the variable count directly and running the next update cycle with the updated count variable.

It's worth having a look at the useState functional update documentation.

For example, if you call setCount twice in the button's event handler, or make use of event bubbling, the first update will be lost.

Incorrect event bubbling usage

// Wrong
const MyComponent = () => {
  const [count, setCount] = useState(0);
  
  return (
    <form onClick={() => setCount(count + 1)}>
      Count: {count}
      <button
        type="button"
        onClick={() => setCount(count + 1)}
      >
        Increment count
      </button>
    </form>
  );
};

This example is a little contrived, but is totally possible in practise where you have complex functions in your event handler calling other functions which ultimately call setCount.

You might expect that both onClick event handlers will run one after the other, resulting in count being equal to 2 after one click on the button. However since both event handlers run within the same update cycle, the state update from the first event handler on the button will be lost.

To rectify this mistake, you should use useState's function update instead.

Correct usage

// Correct
const MyComponent = () => {
  const [count, setCount] = useState(0);
  
  return (
    <form onClick={() => setCount(prevCount => prevCount + 1)}>
      Count: {count}
      <button
        type="button"
        onClick={() => setCount(prevCount => prevCount + 1)}
      >
        Increment count
      </button>
    </form>
  );
};

Using this form instead, count will equal 2 after one button click.

Summary

In general, it is much safer to always use the functional form of state update whenever the new value depends on the previous value, as this will be future-proof if/when the code gets changed later on in the project's lifetime.