If you have been learning about React's useState hook, you have probably seen two different ways to update state and wondered when to use each one.
setCount(count + 1) // Direct update
setCount(prev => prev + 1) // Functional update
They look similar, but choosing the wrong one can lead to bugs.
The Core Difference
Direct update uses the current value from your component's render:
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1); // Uses 'count' from this render
};
Functional update uses the most recent state value that React has:
const [count, setCount] = useState(0);
const increment = () => {
setCount(prev => prev + 1); // Uses latest state from React
};
When It Actually Matters
Multiple Updates in the Same Function
This is where you'll first notice the difference:
const handleClick = () => {
setCount(count + 1); // count = 0, sets to 1
setCount(count + 1); // count is STILL 0, sets to 1 again
setCount(count + 1); // count is STILL 0, sets to 1 again
};
// Result: count becomes 1, not 3
With functional updates:
const handleClick = () => {
setCount(prev => prev + 1); // 0 → 1
setCount(prev => prev + 1); // 1 → 2
setCount(prev => prev + 1); // 2 → 3
};
// Result: count becomes 3
Stale Closures in Async Operations
The bigger problem happens with closures - functions that "remember" values from when they were created.
Example: setTimeout
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setTimeout(() => {
setCount(count + 1); // Uses count from when setTimeout was called
}, 3000);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Add after 3 seconds</button>
</div>
);
}
What happens:
- Count is 0
- Click button (starts timer, remembers count = 0)
- Click button again (starts another timer, also remembers count = 0)
- Wait 3 seconds...
- Both timers execute with count = 0
- Result: count = 1 (not 2!)
Fixed with functional update:
setTimeout(() => {
setCount(prev => prev + 1); // Uses count when timer executes
}, 3000);
Now clicking twice gives you count = 2.
When Either Works Fine
For simple, single updates in response to user actions, both are identical:
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
// These do exactly the same thing
The Rule
Use functional updates setState(prev => ...) when:
- New state depends on previous state - incrementing, decrementing, toggling
- Multiple setState calls in the same function
- Inside async operations - setTimeout, setInterval, fetch callbacks
- Inside useEffect with empty or limited dependencies
- When in doubt - it never hurts to use functional form
Use direct updates (setState(value)) when:
- Setting a completely new value that doesn't depend on previous state
setCount(0) // Reset
setName(event.target.value) // From input
setData(responseData) // From API
setIsOpen(false) // Set to specific value