![]()
A common React pitfall is trying to access an updated state value immediately after calling setState or the state setter from useState. This doesn’t work because state updates are asynchronous in React.
The Problem
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // ❌ Will log the old value
};
return <button onClick={handleClick}>Count: {count}</button>;
}
Why this happens:
- State updates are asynchronous
- React batches state updates for performance
- The
countvariable is constant within each render - Console.log captures the value at call time
Correct Solutions
1. Use the Value in Rendering (Recommended)
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // Update state
};
// ✅ Correct - The render will show updated state
return <button onClick={handleClick}>Count: {count}</button>;
}
2. Use Functional Updates for Calculations
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// ✅ Get previous state in functional update
setCount(prevCount => {
const newCount = prevCount + 1;
console.log(newCount); // Logs correct value
return newCount;
});
};
return <button onClick={handleClick}>Count: {count}</button>;
}
3. Use useEffect for Side Effects
function Counter() {
const [count, setCount] = useState(0);
// ✅ Runs after count changes
useEffect(() => {
console.log('Count updated:', count);
}, [count]);
const handleClick = () => {
setCount(count + 1);
};
return <button onClick={handleClick}>Count: {count}</button>;
}
Class Component Behavior
The same applies to class components:
class Counter extends React.Component {
state = { count: 0 };
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // ❌ Old value
};
// ✅ Use callback for post-update logic
handleClickCorrect = () => {
this.setState(
{ count: this.state.count + 1 },
() => console.log(this.state.count) // ✅ Updated value
);
};
render() {
return <button onClick={this.handleClick}>Count: {this.state.count}</button>;
}
}
Why State Updates Are Asynchronous
React batches state updates because:
- Performance optimization
- Consistency between props and state
- Avoid unnecessary re-renders
- Prepare for concurrent rendering features
Common Mistakes
- Multiple consecutive updates:
const [value, setValue] = useState(0);
const handleClick = () => {
setValue(value + 1);
setValue(value + 1); // ❌ Both use same stale value
};
- Using state for calculations:
const [count, setCount] = useState(0);
const doubleCount = count * 2; // ✅ Safe (derived state)
const handleClick = () => {
setCount(count + 1);
console.log(doubleCount); // ❌ Based on stale count
};
- Async operations with stale state:
const [data, setData] = useState([]);
const fetchData = async () => {
const result = await fetch('/api');
setData(result);
processData(data); // ❌ Uses old data
};
Best Practices
- Treat state as immutable – Never mutate directly
- Use functional updates when new state depends on old state
- Put side effects in useEffect to respond to changes
- Extract values before async operations if needed
- Use useReducer for complex state logic
Advanced Patterns
1. Refs for Current Values
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // Update ref on change
}, [count]);
const handleClick = () => {
const newCount = count + 1;
setCount(newCount);
someFunction(countRef.current); // ✅ Gets latest value
};
}
2. Custom Hook for Derived State
function useDerivedState(initialValue, transform) {
const [state, setState] = useState(initialValue);
const derived = useMemo(() => transform(state), [state]);
return [state, setState, derived];
}
// Usage
function Component() {
const [count, setCount, doubled] = useDerivedState(0, n => n * 2);
// doubled always in sync
}
