Optimizing Event Handlers with useCallback
A common performance oversight in React is not using useCallback
for event handlers, which can lead to unnecessary re-renders of child components.
The Problem: Unoptimized Handlers
function ParentComponent() {
const [count, setCount] = useState(0);
// ❌ New function created on every render
const handleClick = () => {
setCount(count + 1);
};
return <ChildComponent onClick={handleClick} />;
}
Why This Matters
- New function identity on each render
- Child components re-render unnecessarily
- Performance impact in large component trees
The Solution: useCallback
function ParentComponent() {
const [count, setCount] = useState(0);
// ✅ Memoized callback
const handleClick = useCallback(() => {
setCount(c => c + 1); // Using functional update
}, []); // Empty dependencies = stable function
return <ChildComponent onClick={handleClick} />;
}
When to Use useCallback
1. Passing Callbacks to Optimized Children
const MemoizedChild = React.memo(ChildComponent);
function Parent() {
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
return <MemoizedChild onClick={handleClick} />;
}
2. Event Handlers with Dependencies
function Search({ query }) {
const [results, setResults] = useState([]);
// ✅ Recreates only when query changes
const search = useCallback(async () => {
const data = await fetchResults(query);
setResults(data);
}, [query]);
return (
<>
<button onClick={search}>Search</button>
<ResultsList data={results} />
</>
);
}
3. In Dependency Arrays
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [handleResize]); // Needs stable function reference
When NOT to Use useCallback
- Simple components that rarely re-render
- Local event handlers not passed as props
- Cases where optimization costs outweigh benefits
function SimpleButton() {
// ❌ Unnecessary - no child components
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
return <button onClick={handleClick}>Click</button>;
}
Advanced Patterns
1. Dynamic Handlers with Parameters
function ItemList({ items }) {
const handleSelect = useCallback((itemId) => {
console.log('Selected:', itemId);
}, []);
return items.map(item => (
<Item key={item.id} item={item} onSelect={handleSelect} />
));
}
2. Combining with useMemo
function ComplexComponent({ users }) {
const [query, setQuery] = useState('');
const filteredUsers = useMemo(() => {
return users.filter(u => u.name.includes(query));
}, [users, query]);
const handleSearch = useCallback((searchTerm) => {
setQuery(searchTerm);
}, []);
return (
<>
<SearchBar onSearch={handleSearch} />
<UserList users={filteredUsers} />
</>
);
}
Best Practices
- Profile first – Don’t optimize prematurely
- Use React.memo with
useCallback
for child components - Keep dependencies accurate in the dependency array
- Consider context for deeply nested callbacks
- Use the functional update pattern when state depends on previous state
Remember that useCallback
is a performance optimization tool – use it judiciously where it provides measurable benefits, not everywhere by default.