A common React performance pitfall is passing objects to setState()
(or state setters in functional components) without creating new references, which can lead to unexpected behavior and missed updates.
The Problem
// ❌ Wrong - mutating existing state object
const [state, setState] = useState({ count: 0 });
function increment() {
state.count += 1; // Direct mutation
setState(state); // Same object reference
}
Why this fails:
- React uses shallow comparison to detect state changes
- Same object reference appears as “no change”
- Component won’t re-render properly
- Can cause subtle bugs in your application
Correct Solutions
1. Spread Operator (Shallow Copy)
// ✅ Correct - creates new object reference
setState(prevState => ({
...prevState, // Copy existing properties
count: prevState.count + 1 // Update specific property
}));
2. Object.assign (Alternative to Spread)
// ✅ Correct - creates new object
setState(prevState => Object.assign({}, prevState, {
count: prevState.count + 1
}));
3. Functional Updates (Recommended for Derived State)
// ✅ Correct - functional update ensures latest state
setState(prevState => {
return {
...prevState,
count: prevState.count + 1
};
});
4. For Class Components
// ✅ Correct class component usage
this.setState(prevState => ({
...prevState,
count: prevState.count + 1
}));
When You Can Reuse References
There’s one exception where you don’t need new references – when using state management libraries that handle immutability internally:
// With libraries like Immer, Redux, etc.
import produce from 'immer';
setState(produce(draft => {
draft.count += 1; // Safe mutation - library handles copying
}));
Common Mistakes to Avoid
- Nested mutations without proper copying:
// ❌ Wrong - nested mutation
state.user.profile.name = "New Name";
setState(state);
- Partial updates without copying:
// ❌ Wrong - loses other state properties
setState({ count: state.count + 1 });
- Array mutations:
// ❌ Wrong - direct array mutation
state.items.push(newItem);
setState(state);
Performance Considerations
- Shallow copies are cheap for most state objects
- Deep cloning is expensive – avoid unless necessary
- Structural sharing (used by libraries like Immer) helps performance
- Memoization can help with derived state
// For deep objects, consider specialized libraries
import { produce } from 'immer';
function Component() {
const [state, setState] = useState({
user: {
profile: {
name: "John",
preferences: {}
}
}
});
const updateName = () => {
setState(produce(draft => {
draft.user.profile.name = "Jane"; // Safe deep update
}));
};
}