React
useCallback: the use that pays and the noise
useCallback keeps a function's identity stable between renders. It pays when that function is a dependency in an effect or a prop of a memoised child.
Key points
- useCallback does not make the function faster: it only changes the reference between renders.
- The gain arrives when the identity is an effect dependency or a prop of a memoised child.
- The exhaustive-deps rule stays a warning: sometimes the fix is to remove the useCallback.
useCallback returns the same function between two renders as long as the dependencies do not change. It pays when that identity is an effect dependency or a prop of a memoised child; everywhere else it is noise.
What changes and what does not
The function does not get faster: only its identity changes. React compares the reference with the previous render and reuses the old one when the dependencies match. The body still runs on every call, exactly as before.
The three cases where it pays
- Effect dependency: a stable function avoids resubscribing or restarting a timer on every render.
- Prop of a memoised component: without a stable identity the comparison fails and the memo does nothing.
- Dependency of another hook:
useMemoanduseCallbackchain together, and an unstable identity spreads the recomputation.
The real case: a debounced search
function Search({ onQuery }) {
const [input, setInput] = useState("");
const commit = useCallback((value) => onQuery(value), [onQuery]);
useEffect(() => {
const id = setTimeout(() => commit(input), 250);
return () => clearTimeout(id);
}, [input, commit]);
return (
setInput(event.target.value)} />
);
}
Without useCallback, commit would be a new function on every render and the effect would restart on every keystroke, clearing the timer before it fires. With a stable dependency the timer starts once per typed value.
The noise
Wrapping every handler in useCallback with empty dependencies adds a list to maintain and hides the variables the function reads: when the body uses input without declaring it, the value stays stuck at the first render. The gain is zero until something compares that identity.
Checking the dependencies
npm install --save-dev eslint eslint-plugin-react-hooks
npx eslint src
The plugin's react-hooks/exhaustive-deps rule, under the MIT licence, lists the missing dependencies; it is a warning, not an automatic fix. Sometimes the right answer is to drop the useCallback, not to add the dependency.
Live records in the catalogue
A concrete case is filters, where change handlers pass down to child components, and expandable-tabs, where the tab change callback crosses a memoised component. The rest sits among the 111,985 components and the 1,556 blocks.
The limitation
useCallback does not reduce the browser's work: when the child is not memoised and the identity never lands in a dependency array, the function is recreated and compared for no gain. The cost is one more dependency list to keep correct by hand.
Where to continue
The component that benefits from a stable function is in React.memo; the rules the linter enforces are in rules of hooks; the comparison with the other widely used framework is in Vue 3 vs React.
Frequently asked questions
useCallback or useMemo: which one?
useMemo keeps a computed value, useCallback keeps a function. When the result is a function, useCallback is the shorthand for useMemo wrapping that function.
Can I use useCallback with no dependencies?
Yes, an empty array keeps the function for the lifetime of the component. That is correct only when the body reads no props or state: otherwise it captures the first render and never updates.
Does useCallback make the code faster?
No. The body runs on every call exactly as before; only the function reference changes. The speed comes from the child that stops re-rendering because the identity is stable.
Do I still need useCallback with the React compiler?
Much less. The compiler applies automatic memoisation and preserves identities where needed, so the manual use stays for the cases a measurement has shown to be slow.
Why does my function still change on every render?
Because a dependency in the list is unstable: an object created during render, a prop that changes or another function that is not memoised. While that dependency changes, useCallback returns a new function.