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.