Functional State Updates
In this page:
setState((previous) => newValueBasedOn(previous));
setCount((prev) => prev + 1);
The Problem with Stale Values
When you call a state setter using a direct value like setCount(count + 1) multiple times in the same function, each call uses the SAME count value captured from that render -- they don't stack up the way you might expect.
This can cause updates to be silently lost.
setCount(count + 1) twice in a row only increments by 1 total, not 2, because both calls see the same stale count.Example: The Problem with Stale Values
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function BuggyCounter() {
const [count, setCount] = React.useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1); // Still uses the same stale count -- only +1 total!
}
return <button onClick={handleClick}>{count}</button>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<BuggyCounter />);
</script>
</body>
</html>
Using the Functional Update Form
Passing a function to the setter instead of a direct value lets React give you the truly latest state value as an argument, even across multiple calls in the same handler. Each call correctly builds on the result of the previous one.
prev (short for 'previous value'), like setCount(prev => prev + 1).Example: Using the Functional Update Form
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function FixedCounter() {
const [count, setCount] = React.useState(0);
function handleClick() {
setCount(prev => prev + 1);
setCount(prev => prev + 1); // Correctly adds +2 total
}
return <button onClick={handleClick}>{count}</button>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<FixedCounter />);
</script>
</body>
</html>
Functional Updates Inside Timers and Callbacks
The functional update form is especially important inside setTimeout, event listeners, or other callbacks that might run after some delay -- by that point, a directly-captured state variable could be outdated.
The functional form always gets the current value when it actually runs.
setTimeout, setInterval, or a Promise .then() is a strong candidate for the functional update form.Example: Functional Updates Inside Timers and Callbacks
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function DelayedIncrement() {
const [count, setCount] = React.useState(0);
function incrementLater() {
setTimeout(() => setCount(prev => prev + 1), 500);
}
return <button onClick={incrementLater}>Count: {count}</button>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<DelayedIncrement />);
</script>
</body>
</html>
- Using the direct value form (
setCount(count + 1)) when updating state multiple times in a row based on the previous value. - Assuming the functional form is only needed for counters -- it applies to any state update that depends on the previous value.
- Forgetting the updater function receives the LATEST state as its argument, not necessarily the value from when the handler was defined.
- The functional form of a state setter takes a function instead of a direct value.
- That function receives the most up-to-date previous state as its argument.
- It's the safe way to update state multiple times in a row or inside closures.
- It avoids bugs caused by referencing a stale value from an earlier render.
No browser-specific restrictions -- available for useState/useReducer since Hooks were introduced in React 16.8.
Chapter Quiz — Complete all 10 topics to unlock
0/10 topics done
Complete these topics first: