useCallback
Every time a React component renders, the functions defined inside it are created anew. Most of the time that is harmless, but a fresh function identity can defeat optimizations like React.memo or trigger effects that depend on the function. useCallback returns a memoized version of a callback that only changes when one of its dependencies changes, giving you a stable reference across renders. It is a targeted performance tool — not something to sprinkle everywhere.
How useCallback works
useCallback takes a function and a dependency array, and returns the same function instance between renders as long as the dependencies are unchanged. When a dependency changes, it returns a new function.
import { useCallback, useState } from "react";
function SearchBox({ onSearch }) {
const [query, setQuery] = useState("");
const handleSubmit = useCallback(
(event) => {
event.preventDefault();
onSearch(query);
},
[onSearch, query]
);
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="submit">Search</button>
</form>
);
}
Here handleSubmit keeps the same identity until onSearch or query changes. Conceptually, useCallback(fn, deps) is equivalent to useMemo(() => fn, deps) — it memoizes the function itself rather than the result of calling it.
Pairing with React.memo
The clearest payoff comes when you pass callbacks to children wrapped in React.memo. A memoized child skips re-rendering when its props are referentially equal. If you hand it a brand-new function on every parent render, the memoization is useless because the prop always looks “new.”
import { memo, useCallback, useState } from "react";
const Button = memo(function Button({ onClick, label }) {
console.log("render:", label);
return <button onClick={onClick}>{label}</button>;
});
function Counter() {
const [count, setCount] = useState(0);
const [theme, setTheme] = useState("light");
const increment = useCallback(() => setCount((c) => c + 1), []);
return (
<div className={theme}>
<p>Count: {count}</p>
<Button onClick={increment} label="Increment" />
<button onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}>
Toggle theme
</button>
</div>
);
}
Output:
render: Increment // logged once on mount
// toggling the theme no longer re-renders <Button>
Because increment is stable (empty dependency array, with the functional updater avoiding a count dependency), toggling the theme re-renders Counter but not Button. Remove the useCallback and Button would log on every theme toggle.
Note:
useCallbackonly helps if the child is actually memoized. Passing a stable callback to a plain, un-memoized component changes nothing — that component re-renders with its parent regardless.
Dependency arrays
The dependency array tells React when to recreate the function. Every reactive value the function reads — props, state, or other variables from the component scope — should appear in it. Omitting a dependency leads to stale closures where the callback captures an old value.
// Stale: captures the first `userId` forever
const load = useCallback(() => fetchUser(userId), []); // wrong
// Correct: refreshes when userId changes
const load = useCallback(() => fetchUser(userId), [userId]);
When you only need the previous state, prefer the functional updater form (setCount(c => c + 1)) so you can keep the array empty and the function maximally stable. The eslint-plugin-react-hooks exhaustive-deps rule catches missing dependencies for you.
useCallback vs useMemo
Both memoize, but they cache different things.
| Aspect | useCallback | useMemo |
|---|---|---|
| Returns | A memoized function | A memoized computed value |
| Equivalent | useMemo(() => fn, deps) | — |
| Typical use | Stable callbacks for memoized children / effect deps | Expensive calculations, stable object/array props |
| Argument | The function to cache | A factory that produces the value |
Use useCallback when the function reference itself matters. Use useMemo when the result of a computation is expensive or when you need a stable non-function value (such as a derived array passed to a memoized child).
Overuse pitfalls
useCallback is not free. It allocates a dependency array on every render and runs a comparison, and it adds noise to your code. Wrapping every handler “just in case” usually costs more than it saves.
- It does nothing useful unless the consumer is memoized or the function is an effect/hook dependency.
- An incorrect dependency array can hide subtle stale-value bugs that are harder to debug than an extra render.
- If
countis in the dependencies, the function still changes whenevercountchanges — so the memoization buys you nothing in that case.
Warning: Reach for
useCallbackto fix a measured performance problem, not preemptively. Profile with the React DevTools Profiler before adding it.
Best Practices
- Use
useCallbackonly when the callback flows intoReact.memochildren, custom hooks, or dependency arrays ofuseEffect/useMemo. - List every reactive value the callback reads in the dependency array; trust
exhaustive-depsover manual judgment. - Prefer functional state updaters to shrink dependency arrays and keep callbacks stable.
- Remember that
useCallbackis pointless if the child receiving the function is not memoized. - Combine it with
useMemofor objects/arrays andReact.memofor components to make memoization actually effective. - Don’t optimize blindly — measure first, then memoize the hot paths.