useCallback — Core Concepts, the Function Memoization Hook in React

Author: IT Sectr Published: 2026-07-04 Reading time: 9 min

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 — a hook for memoizing a function, preserving the reference between renders.
  • React.memo — useCallback is only effective with React.memo to prevent re-renders.
  • Dependencies — the function is recreated only when the specified values in the array change.
  • Stable Reference — useCallback guarantees the function reference won't change unnecessarily.
  • Don't Overuse — excessive useCallback without React.memo hurts performance.

What is useCallback in React

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.

jsx
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} />;
}

How Function Memoization Works

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.

ScenarioWithout useCallbackWith useCallback
Function CreationNew on every renderSame with stable deps
Passed to React.memoChild re-rendersChild doesn't re-render
In useEffect arrayEffect restartsEffect stays stable
OverheadMinimalDependency comparison + memory

useCallback and Performance

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.

jsx
// 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.

When to Use useCallback

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.

  • React.memo children — prevents re-rendering of memoized child components when passing callbacks.
  • useEffect dependencies — stabilizes the function in the effect dependency array, preventing unnecessary restarts.
  • Custom hooks — if a hook accepts a callback and depends on its reference equality, useCallback is required.
  • Context value — if a function is passed in the context value, useCallback stabilizes the reference.
jsx
// 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

useCallback vs useMemo

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.

HookMemoizesSyntaxUsage
useCallbackFunction (reference)useCallback(fn, deps)Callbacks for child components
useMemoComputation resultuseMemo(() => value, deps)Expensive computations, object memoization
jsx
// These are equivalent:
const handleClick = useCallback(() => doSomething(a, b), [a, b]);
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);

Common useCallback Mistakes

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.

  • useCallback without React.memo — child component still re-renders; optimization is pointless.
  • Missing dependencies — if a variable used inside the callback is not listed in deps, the callback contains a stale closure.
  • Excessive memoization — wrapping every function, including trivial onClick handlers with console.log.
  • Objects and arrays in dependencies — a new reference on each render is considered a change, making memoization useless.
jsx
// ❌ 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

Should every function be wrapped in useCallback?

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.

Which is faster — useCallback or a new function on every render?

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.

Can useCallback be used with async functions?

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) => {...}, []).

How to debug problems with useCallback and React.memo?

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.

How to pass useCallback to a child component without React.memo?

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

  • useCallback — a hook for memoizing a function, returning a stable reference until dependencies change.
  • React.memo synergy — useCallback is only effective with React.memo on child components.
  • Dependency comparison — via Object.is; objects/arrays in deps break memoization.
  • Measure, don't guess — add useCallback only after detecting a bottleneck via the profiler.
  • Stable closure — include all used variables in the dependency array, otherwise — stale closure.
  • useCallback vs useMemo — useCallback memoizes a function, useMemo memoizes a computation result.
  • Overuse — over 90% of useCallback usage in real-world projects is premature optimization.

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.

Discuss the project

Read also