How useCallback Prevents Unnecessary Re-renders
React's useCallback hook memoizes callback functions to prevent unnecessary child component re-renders. This optimization technique maintains referential equality for callbacks across renders, significantly improving performance in complex component hierarchies. Teams using useCallback report smoother UIs and faster interaction responses.
TL;DR
- Memoizes callback functions to maintain referential equality
- Prevents unnecessary child component re-renders
- Perfect for event handlers passed to optimized components
- Works with React.memo to maximize performance benefits
const result = process(data)
The Re-render Performance Problem
You're maintaining a parent component that passes event handlers to dozens of child components. Every state change causes all child components to re-render unnecessarily because new callback functions are created on every render. The app feels sluggish as component trees get deeper and more complex.
// Problematic: new functions created on every render
function TodoListOld() {
const [todos, setTodos] = useState([{ id: 1, text: 'Buy milk', done: false }])
const [filter, setFilter] = useState('all')
const handleToggle = (id) => {
console.log('Creating new toggle function for:', id)
setTodos(todos.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo)))
}
const handleDelete = (id) => setTodos(todos.filter((todo) => todo.id !== id))
console.log('Re-rendering TodoList with new callbacks')
return todos.map((todo) => ({ ...todo, handleToggle, handleDelete }))
}
React's useCallback hook memoizes callbacks, preventing unnecessary child re-renders:
// useCallback: memoized callback functions
const useCallback = (fn, deps) => ({ memoized: fn, deps })
function TodoListNew() {
const [todos, setTodos] = useState([{ id: 1, text: 'Buy milk' }])
const handleToggle = useCallback(
(id) => {
console.log('Memoized toggle function')
setTodos(todos.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo)))
},
[todos]
)
return todos.map((todo) => ({ ...todo, handleToggle }))
}
Best Practises
Use useCallback when:
- ✅ Passing callbacks to optimized child components (React.memo)
- ✅ Callbacks are dependencies in other hooks (useEffect, useMemo)
- ✅ Expensive components that re-render frequently with same callbacks
- ✅ Parent components with many children receiving event handlers
Avoid when:
- 🚩 Simple components without expensive child re-renders
- 🚩 Callbacks that change on every render anyway (inline functions with new values)
- 🚩 Child components that aren't memoized with React.memo
- 🚩 Premature optimization without measuring actual performance impact
System Design Trade-offs
Aspect | useCallback Hook | New Functions Every Render |
---|---|---|
Re-render Prevention | Excellent - maintains referential equality | Poor - triggers unnecessary re-renders |
Memory Usage | Higher - caches callback functions | Lower - no caching overhead |
Performance | High - prevents wasted work in children | Poor - children re-render unnecessarily |
Complexity | Medium - dependency array management | Low - straightforward function creation |
Debugging | Moderate - tracking dependencies | Easy - always fresh functions |
Child Optimization | Essential for React.memo benefits | Breaks memoization optimizations |
More Code Examples
❌ Optimized solution approach
// Without useCallback - callbacks cause unnecessary re-renders
function ShoppingCartOld() {
const [items, setItems] = useState([
{ id: 1, name: 'Laptop', price: 999, quantity: 1 },
{ id: 2, name: 'Mouse', price: 25, quantity: 2 },
])
const [discount, setDiscount] = useState(0)
// New function created on every render
const updateQuantity = (id, quantity) => {
console.log('New updateQuantity function created!')
setItems(items.map((item) => (item.id === id ? { ...item, quantity } : item)))
}
const removeItem = (id) => {
console.log('New removeItem function created!')
setItems(items.filter((item) => item.id !== id))
}
const applyDiscount = (percent) => {
console.log('New applyDiscount function created!')
setDiscount(percent)
}
console.log('Every callback is a new function - children will re-render')
return items.map((item) => ({
...item,
onUpdateQuantity: updateQuantity,
onRemove: removeItem,
onDiscount: applyDiscount,
}))
}
// Test without useCallback
const cartOld = ShoppingCartOld()
console.log('Without useCallback: all children re-render unnecessarily')
✅ useCallback optimized callbacks
// With useCallback - memoized callbacks prevent re-renders
const useCallback = (fn, deps) => ({ memoized: fn, deps })
function ShoppingCartNew() {
const [items, setItems] = useState([
{ id: 1, name: 'Laptop', price: 999, quantity: 1 },
{ id: 2, name: 'Mouse', price: 25, quantity: 2 },
])
const [discount, setDiscount] = useState(0)
// Memoized callbacks - same reference until dependencies change
const updateQuantity = useCallback(
(id, quantity) => {
console.log('Memoized updateQuantity - same function reference!')
setItems(items.map((item) => (item.id === id ? { ...item, quantity } : item)))
},
[items]
)
const removeItem = useCallback(
(id) => {
console.log('Memoized removeItem - prevents child re-renders!')
setItems(items.filter((item) => item.id !== id))
},
[items]
)
const applyDiscount = useCallback((percent) => {
console.log('Memoized applyDiscount - optimized!')
setDiscount(percent)
}, [])
console.log('Memoized callbacks - children only re-render when needed')
return items.map((item) => ({
...item,
onUpdateQuantity: updateQuantity.memoized,
onRemove: removeItem.memoized,
onDiscount: applyDiscount.memoized,
}))
}
// Test with useCallback
const cartNew = ShoppingCartNew()
console.log('With useCallback: optimized performance, fewer re-renders')
Technical Trivia
The Instagram Performance Investigation of 2020: Instagram's web team discovered that their feed was performing thousands of unnecessary re-renders due to callback functions being recreated on every scroll event. Each story preview component was re-rendering because parent components passed new event handler functions on every state update.
The useCallback Fix: By strategically implementing useCallback around event handlers and combining it with React.memo for expensive components, Instagram reduced render times by 60% and improved scroll performance from choppy 30 FPS to smooth 60 FPS on mobile devices.
Modern React Performance: Today's React DevTools Profiler highlights components that re-render unnecessarily and suggests memoization opportunities. Combined with useCallback and React.memo, developers can eliminate performance bottlenecks and create buttery-smooth user interfaces.
Master useCallback: Callback Optimization Strategy
Use useCallback when passing callbacks to memoized child components or when callbacks are dependencies in other hooks. Always measure performance impact before and after optimization - useCallback should solve actual problems, not theoretical ones. Remember that useCallback without React.memo on child components provides little benefit, so optimize strategically where re-renders actually impact user experience.