useCallback is a React hook that returns a memoized version of a function that doesn't change between renders until its dependencies change. Unlike a regular function declaration inside a component (which creates a new function on every render), useCallback stabilizes the function reference, preventing unnecessary re-renders of child components optimized with React.memo. According to React Documentation (2025), useCallback is only useful in combination with React.memo or hooks that depend on a stable reference.
Key Takeaways
useCallback is a hook added in React 16.8 that memoizes a function: it returns the same reference until the dependencies change. Without useCallback, every function declaration inside a component creates a new function object on each render. For primitives this is unnoticeable, but when passing such callbacks to child components optimized with React.memo, each new reference causes a re-render of the child component.
Syntactically, useCallback is equivalent to useMemo for a function: useCallback(fn, deps) is shorthand for useMemo(() => fn, deps). React stores the memoized function in the fiber node's internal storage and compares dependencies on each render. If the dependencies haven't changed (Object.is for each item), the previous function is returned.
According to React Documentation — useCallback (2025), you shouldn't wrap every function in useCallback. The hook has its cost: calling the hook, comparing dependencies, and allocating memory for the dependency array. If a component is simple and doesn't have deep trees with React.memo, useCallback will only slow down the application. Optimization should be measurable, not intuitive.
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// Stable reference — same function until deps change
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return <Child onClick={handleClick} />;
}
Memoization in useCallback is based on caching the function call result. React preserves the closure created at the time of the first render and returns it on subsequent renders as long as the dependencies remain unchanged. Inside the fiber node, each useCallback call creates a node in the linked list of hooks, where previous dependencies and the memoized value are stored.
The dependency comparison is performed strictly via Object.is — a shallow comparison without deep checking of objects or arrays. If a dependency is an object or array, a new reference on each render will be considered a change. Therefore, the dependency array should contain primitive values or stable references (e.g., from useRef or useMemo).
According to React Core Team — Optimization Guide (2024), the cost of memoization includes three components: allocating the dependency array on each render, iterating and comparing elements via Object.is, and potential garbage collection overhead when recreating. For a component with hundreds of useCallback wrappers, this can become noticeable — so selectivity in using the hook is critical.
| Scenario | Without useCallback | With useCallback |
|---|---|---|
| Function Creation | New on every render | Same with stable deps |
| Passed to React.memo | Child re-renders | Child doesn't re-render |
| In useEffect array | Effect restarts | Effect stays stable |
| Overhead | Minimal | Dependency comparison + memory |
There is a widespread myth that useCallback automatically improves performance. In reality, in isolation (without React.memo), useCallback even slightly slows down the application due to the cost of dependency comparison. The hook only provides real benefit in three scenarios: preventing re-renders of React.memo components, stabilizing callbacks in useEffect, and passing callbacks to custom hooks that depend on reference equality.
The rule is simple: until you detect a performance problem using React DevTools Profiler, don't use useCallback. The React team has repeatedly emphasized that premature optimization is the root of all evil. First write clean code without memoization, measure it, find the bottleneck in the profiler, and only then add useCallback where it's truly needed.
// Measurable optimization: Child is wrapped in React.memo
const Child = React.memo(({ onClick }) => {
console.log('Child re-rendered');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <Child onClick={handleClick} />;
}
According to Dan Abramov — Before You memo() (2024), over 90% of useCallback usage in open-source projects is redundant. Developers wrap every function "just in case" without measuring the effect. The alternative: if the child component is heavy and its re-render is expensive — React.memo + useCallback is justified. If the child component is lightweight — re-rendering is cheaper than dependency comparison.
The first scenario is React.memo. If a child component is wrapped in React.memo and receives a callback function as a prop, without useCallback the child component will re-render on every parent render, even if its own data hasn't changed. useCallback stabilizes the reference, allowing React.memo to correctly skip the re-render.
The second scenario is useEffect with a callback in dependencies. If a function is passed to the useEffect dependency array, each new reference will restart the effect. useCallback ensures the reference is stable, and the effect only runs when the actual data changes, not on every render. This is especially important for subscriptions and requests.
// useCallback for stable useEffect dependency
const fetchData = useCallback(async (id) => {
const res = await fetch(`/api/${id}`);
setData(res.data);
}, []); // stable reference, never re-creates
useEffect(() => {
fetchData(props.id);
}, [props.id, fetchData]); // effect runs only when props.id changes
The main difference between useCallback and useMemo is what each one memoizes. useCallback memoizes a function: useCallback(fn, deps) returns fn (the same or the previous version). useMemo memoizes the result of calling a function: useMemo(() => computeExpensive(a, b), [a, b]) returns the computed value, not a function.
Technically, useCallback is syntactic sugar over useMemo: useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). This syntax exists only for readability — so the developer can clearly see that a function is being memoized, not a value. There is no performance difference between useCallback and useMemo with a function — they generate identical code.
| Hook | Memoizes | Syntax | Usage |
|---|---|---|---|
| useCallback | Function (reference) | useCallback(fn, deps) | Callbacks for child components |
| useMemo | Computation result | useMemo(() => value, deps) | Expensive computations, object memoization |
// These are equivalent:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);
The most common mistake is meaningless wrapping of all functions in useCallback without React.memo on child components. If a child component is not wrapped in React.memo, it will still re-render on every parent render, regardless of whether the callback reference changes or not. useCallback without React.memo is cost without benefit.
// ❌ Useless: no React.memo on child
const handle = useCallback(() => doStuff(), []);
<Child onClick={handle} />; // Child still re-renders without React.memo
// ❌ Stale closure: missing dependency
const handle = useCallback(() => {
console.log(count); // count is always 0 — stale closure!
}, []);
// ✅ Correct: include dependencies
const handle = useCallback(() => {
console.log(count);
}, [count]);
The stale closure problem in useCallback is solved by including all used variables in the dependency array. eslint-plugin-react-hooks with exhaustive-deps automatically checks that all variables from the callback body are present in the array. If the callback uses setState, which doesn't change between renders, you can safely include it in deps — React guarantees the stability of setState.
Frequently Asked Questions
No. useCallback only makes sense in three cases: the child component is wrapped in React.memo, the function is used in the useEffect dependency array, or the function is passed to a custom hook that depends on reference equality. In all other cases, useCallback adds overhead without benefit. The React team recommends writing without optimizations first and adding them based on profiling results.
For simple components — a new function on every render is slightly faster, since useCallback spends resources on dependency comparison and array allocation. For components with deep React.memo trees, useCallback wins by preventing re-renders of thousands of child elements. Measure and compare instead of guessing — use the React DevTools Profiler for objective evaluation.
Yes, useCallback works with async functions exactly the same as with synchronous ones. The hook memoizes the function itself, and the result (a Promise) is returned each time it's called. An async function inside useCallback is a common pattern for stable data fetching callbacks used in useEffect: const fetchData = useCallback(async (id) => {...}, []).
Use React DevTools Profiler — it shows which components re-render and why. For programmatic checking, add console.log or use useWhyDidYouUpdate — a library that logs the reason for re-render. The main reasons: a prop changed (including a callback reference), state changed, or context changed. If useCallback doesn't help — check that all dependencies are correctly specified.
Even without React.memo, useCallback can be useful in combination with useMemo for context values. If you're passing an object with functions to Context.Provider, wrap the object creation in useMemo, and each function in useCallback. This prevents re-rendering of all context consumers when one of the functions changes. But for directly passing callbacks in props without React.memo, there's no benefit from useCallback.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also