Functional State Updates
In this page:
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.
Note: If you ever call the same setter more than once in a row based on the previous value, that's a sign you need the functional form instead.
Warning: Calling 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.
Note: The convention is to name the function's parameter prev (short for 'previous value'), like setCount(prev => prev + 1).
Warning: Mixing functional and direct updates in the same handler can still cause confusion -- prefer the functional form consistently when in doubt.
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.
Note: Any state update inside setTimeout, setInterval, or a Promise .then() is a strong candidate for the functional update form.
Warning: Using a stale, directly-captured value inside a delayed callback is a very common source of subtle React bugs.
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: