useCallback
useCallback returns a memoised function reference that only changes when its dependencies change. Pair with React.memo children or effect dependencies — not by default.
When useCallback actually helps
EXAMPLE
import { useCallback, useState, memo } from 'react';
// 1) Stable handler for a memoised child
const HeavyChild = memo(function HeavyChild({ onChange }) {
/* expensive render */
return <input onChange={onChange} />;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback, onChange is a NEW function each render → HeavyChild rerenders
const onChange = useCallback((e) => {
console.log(e.target.value);
}, []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>+1 {count}</button>
<HeavyChild onChange={onChange} />
</>
);
}
// 2) Stable function in useEffect deps
function Searcher({ query }) {
const [results, setResults] = useState([]);
const fetchResults = useCallback(async () => {
const r = await api.search(query);
setResults(r);
}, [query]);
useEffect(() => { fetchResults(); }, [fetchResults]);
return <List items={results} />;
}
// 3) Passing handlers down through context
const ToggleCtx = createContext();
function Provider({ children }) {
const [on, setOn] = useState(false);
const toggle = useCallback(() => setOn(o => !o), []);
return <ToggleCtx.Provider value={{ on, toggle }}>{children}</ToggleCtx.Provider>;
}
// 4) When NOT to use useCallback
// • The child is NOT React.memo — fresh function refs don't cause perf issues
// • The handler is recreated every render anyway because deps change every render
// • Inline handlers in simple components — useCallback's overhead > savings
// 5) Common bugs
// 5a) Empty deps + closure over state — STALE
const increment = useCallback(() => setCount(count + 1), []);
// Reads count from initial render forever. Fix:
const increment2 = useCallback(() => setCount(c => c + 1), []);
// 5b) Forgetting a dep
const submit = useCallback(() => api.post(formData), []); // formData stale
const submit2 = useCallback(() => api.post(formData), [formData]);
// 5c) Wrapping unstable callbacks in useCallback that depends on them
const stable = useCallback((id) => onSelect(id), [onSelect]);
// If onSelect changes every render, stable changes every render — pointless.
// 6) useCallback vs useMemo
// useMemo(() => computeFn(), [deps]) — memoise the RESULT of calling fn
// useCallback(fn, [deps]) — memoise the FUNCTION reference
// useCallback(fn, deps) === useMemo(() => fn, deps)
// 7) Profile FIRST
// React DevTools Profiler will show which children are re-rendering and why.
// Most apps DON'T need useCallback; over-using it adds noise without speed.
Why it matters
useCallback only helps when its returned function is consumed by something that cares about reference equality — a memo child, a useEffect dep array, or a context value. Otherwise it’s noise.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
const onClick = useCallback(() => save(id), [id]);
return <Button onClick={onClick} />;
Try it Yourself »
Discussion
Loading…