useCallback Hook
Overview
In JavaScript, functions are objects. Every time a component re-renders, it creates a brand new function in memory for every event handler (e.g., const handleClick = () => {}).
Usually, this is fine. But if you pass handleClick as a prop down to a child component wrapped in React.memo, you have a problem. React.memo does a shallow comparison of props. Because the parent created a brand new function in memory during the re-render, the child sees a new memory reference, assumes the prop changed, and re-renders anyway!
`useCallback` is a specialized version of useMemo designed specifically for caching functions. It tells React: 'Cache this function definition. Do not create a new function in memory unless the dependencies change.' This allows you to safely pass stable function references down to heavily memoized child components without triggering useless renders.
Syntax
import { useState, useCallback, memo } from 'react';
// A highly optimized child component
const MegaButton = memo(function MegaButton({ onClick, children }) {
console.log("MegaButton Rendered!");
return <button onClick={onClick}>{children}</button>;
});
function ParentApp() {
const [text, setText] = useState('');
const [score, setScore] = useState(0);
// 1. Without useCallback, this function is rebuilt in memory on every keystroke.
// MegaButton would re-render endlessly.
// const handleScore = () => setScore(s => s + 1);
// 2. With useCallback, the memory reference is locked.
// MegaButton sees the EXACT same function reference and skips rendering!
const handleScore = useCallback(() => {
setScore(s => s + 1);
}, []); // Empty array = never recreate this function
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<MegaButton onClick={handleScore}>Add Score</MegaButton>
</div>
);
}Common Pitfalls
- Stale Closures (Missing Dependencies): If your
useCallbackfunction relies on a state variable, but you forget to put it in the dependency array, the cached function will permanently use the initial state value. For example,useCallback(() => console.log(count), [])will always print '0', even if count is currently 100.
Interview Questions
useMemo and useCallback?They are fundamentally the same under the hood. useMemo executes the provided function and caches the result of the function. useCallback caches the function itself, allowing you to pass the unexecuted function reference down to child components safely. In fact, useCallback(fn, deps) is just syntactic sugar for useMemo(() => fn, deps).
useCallback?It is only necessary in two scenarios: 1) You are passing the function as a prop to a child component optimized with React.memo. 2) The function is being used as a dependency inside a useEffect array, and you want to prevent the effect from triggering infinitely.
Real-World Example
useCallback with useEffect Dependencies: If you define a function outside a useEffect but use it inside, the ESLint rules will force you to list the function in the dependency array. If you don't wrap the function in useCallback, you will cause an infinite network request loop.
function SearchWidget({ query }) {
const [results, setResults] = useState([]);
// The fetch logic is complex, so we abstract it into a function
// We MUST wrap it in useCallback, because we use it inside useEffect
const fetchResults = useCallback(async () => {
const res = await api.search(query);
setResults(res.data);
}, [query]); // Recreate this function ONLY when 'query' changes
useEffect(() => {
// If fetchResults wasn't wrapped in useCallback, it would be a brand new
// function every render. This effect would see a 'new' dependency,
// re-run the fetch, trigger a render, see a 'new' function, and loop infinitely!
fetchResults();
}, [fetchResults]); // The linter forces us to include it here
return <List items={results} />;
}Check Your Knowledge
Test your understanding of useCallback Hook with these quick questions.