A common performance pitfall in React is accidentally passing new object references as props, which triggers unnecessary re-renders of child components. Here’s how to identify and fix these issues:
The Core Problem
When you pass objects, arrays, or functions as props, React uses referential equality to determine if props changed. Even if the content is identical, a new reference forces a re-render:
// This triggers re-render every time, even if values are same
<ChildComponent style={{ color: 'red' }} onClick={() => {}} />
Optimization Techniques
1. Memoize Object Props with useMemo
const buttonStyle = useMemo(() => ({
color: 'red',
padding: '10px'
}), []); // Empty dependency = stable reference
<Button style={buttonStyle} />
2. Memoize Function Props with useCallback
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Stable function reference
<Button onClick={handleClick} />
3. Extract Static Objects Outside Components
// Outside component - single reference
const STATIC_STYLE = { color: 'red' };
function Component() {
return <Child style={STATIC_STYLE} />;
}
4. For Arrays, Use Stable References
const items = useMemo(() => [1, 2, 3], []);
<List items={items} />
5. Custom Comparison with React.memo
const UserCard = React.memo(
({ user }) => <div>{user.name}</div>,
(prevProps, nextProps) => {
// Custom comparison logic
return prevProps.user.id === nextProps.user.id;
}
);
Advanced Patterns
1. Context Selectors for Object State
const user = useContextSelector(UserContext, state => ({
name: state.name,
avatar: state.avatar
}));
2. State Normalization
// Instead of:
setState(prev => ({ ...prev, user: { ...prev.user, name: 'New' } }));
// Normalize to flatten structure:
const [userName, setUserName] = useState('Old');
3. Immutable Update Patterns
// Using libraries like Immer for clean updates
import produce from 'immer';
setState(produce(draft => {
draft.user.name = 'New'; // Doesn't create new references for unchanged parts
}));
Debugging Reference Changes
- React DevTools:
- Highlight updates when components render
- Inspect prop values between renders
- Custom Hook:
function useWhyDidYouUpdate(name, props) {
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
const changes = {};
Object.keys({ ...previousProps.current, ...props }).forEach(key => {
if (previousProps.current[key] !== props[key]) {
changes[key] = { from: previousProps.current[key], to: props[key] };
}
});
if (Object.keys(changes).length) {
console.log('[why-did-you-update]', name, changes);
}
}
previousProps.current = props;
});
}
// Usage in component:
useWhyDidYouUpdate('ComponentName', { prop1, prop2 });
Performance Impact
Scenario | Reference Type | Re-renders Child? |
---|---|---|
Primitive (string/number) | Same value | No |
Object/Array | New reference | Yes |
Object/Array | Same reference | No |
Function | New reference | Yes |
Function | Same reference | No |
Best Practices
- For objects/arrays: Prefer
useMemo
when created inside component - For functions: Always use
useCallback
for event handlers - For context: Select only needed values to avoid upstream changes
- For lists: Ensure stable item references when possible
- For styles: Consider CSS-in-JS solutions that handle memoization