![]()
A critical React mistake is modifying state directly rather than using the proper setState() method (for class components) or state setter function (for functional components).
The Problem
// ❌ Wrong - directly mutating state
this.state.count = 1; // Class component bad
const [state, setState] = useState({ count: 0 });
state.count = 1; // Functional component bad
Why this is problematic:
- React won’t detect the state change
- Components won’t re-render properly
- Can lead to UI/state inconsistencies
- Breaks React’s unidirectional data flow
Correct Solutions
For Class Components
// ✅ Correct - using setState()
this.setState({ count: 1 });
// For state depending on previous state
this.setState(prevState => ({
count: prevState.count + 1
}));
For Functional Components
const [count, setCount] = useState(0);
// ✅ Correct - using state setter
setCount(1);
// For objects in functional components
const [state, setState] = useState({ count: 0 });
// ✅ Correct - create new object
setState(prev => ({ ...prev, count: 1 }));
Key Differences
| Approach | Problem | Correct Method |
|---|---|---|
this.state.x = y | No re-render | this.setState({x: y}) |
state.x = y (functional) | No re-render | setState({...state, x: y}) |
| Array push | No re-render | setArr([...arr, newItem]) |
Common Anti-Patterns to Avoid
- Nested object mutations:
// ❌ Wrong
this.state.user.name = 'Alice';
this.forceUpdate(); // Never do this!
- Array mutations:
// ❌ Wrong
state.items.push(newItem);
setState(state); // Reference is same
- Async state updates:
// ❌ Problematic
this.state.count = this.state.count + 1;
setTimeout(() => {
this.setState({}); // Hacky workaround
}, 0);
Best Practices
- Treat state as immutable – always create new objects/arrays
- Use functional updates when new state depends on previous state
setCount(prev => prev + 1);
- For complex state, consider useReducer
- Use spread operator for objects/arrays:
setUser({ ...user, name: 'Alice' });
setItems([...items, newItem]);
Performance Considerations
- Shallow comparisons – React uses Object.is comparison
- Batching updates – multiple setState calls may be batched
- Object spread vs libraries – for deep objects consider Immer
// With Immer (simpler deep updates)
import produce from 'immer';
setState(produce(draft => {
draft.user.profile.name = 'Alice';
}));
