The Importance of useCallback
for Event Handlers in Child Components
When passing event handlers to child components in React, failing to use useCallback
can lead to performance issues. Here’s what you need to know:
The Problem: Unnecessary Re-renders
Without useCallback
:
function ParentComponent() {
const handleClick = () => {
console.log('Button clicked');
};
return <ChildComponent onClick={handleClick} />;
}
const ChildComponent = React.memo(({ onClick }) => {
return <button onClick={onClick}>Click me</button>;
});
What’s wrong?
- A new
handleClick
function is created on every render React.memo()
optimization is defeated because props keep changing- Child component re-renders unnecessarily
The Solution: useCallback
function ParentComponent() {
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // Empty dependency array = same function
return <ChildComponent onClick={handleClick} />;
}
Why this works:
useCallback
memoizes the function between renders- Same function reference is passed to child component
React.memo()
optimization works as intended
When to Use useCallback
- Passing handlers to optimized children (
React.memo
,PureComponent
) - Handlers used in dependency arrays of other hooks
- Performance-sensitive components with frequent re-renders
Dependency Array Considerations
For handlers that depend on props/state:
function ParentComponent({ userId }) {
const handleClick = useCallback(() => {
console.log('User clicked:', userId);
}, [userId]); // Recreate when userId changes
return <ChildComponent onClick={handleClick} />;
}
Common Mistakes to Avoid
❌ Empty dependencies when values are needed:
const [count, setCount] = useState(0);
// Bad - will always log initial count (0)
const handleClick = useCallback(() => {
console.log(count);
}, []);
✅ Correct with dependencies:
const [count, setCount] = useState(0);
// Good - logs current count
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
❌ Overusing useCallback everywhere:
// Unnecessary for simple components without performance issues
const handleClick = useCallback(() => {...}, []);
Performance Impact
Approach | Function Creation | Child Re-renders |
---|---|---|
No memoization | Every render | Every parent render |
useCallback | When deps change | Only when deps change |
Real-World Example
function App() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
// Stable callback - no dependencies
const addTodo = useCallback((text) => {
setTodos(prev => [...prev, { text, completed: false }]);
}, []);
// Depends on filter - recreates when filter changes
const getVisibleTodos = useCallback(() => {
return todos.filter(todo =>
filter === 'all' ||
(filter === 'completed' && todo.completed) ||
(filter === 'active' && !todo.completed)
);
}, [todos, filter]);
return (
<div>
<TodoForm onSubmit={addTodo} />
<TodoList
todos={todos}
getVisibleTodos={getVisibleTodos}
/>
<FilterControls
currentFilter={filter}
onFilterChange={setFilter}
/>
</div>
);
}
When You Can Skip useCallback
- Simple components that don’t re-render often
- Leaf components with no performance issues
- Cases where the cost of memoization outweighs benefits
Key Takeaways
- Use
useCallback
when passing handlers to optimized child components - Include all dependencies in the dependency array
- Avoid overusing it where it doesn’t provide value
- Combine with
React.memo
for maximum performance benefits - Profile your app to identify real performance bottlenecks
Proper use of useCallback
helps prevent unnecessary re-renders and keeps your React application performant, especially in complex component trees.