Skip to content
React rc hooks 4 min read

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: useCallback only 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.

AspectuseCallbackuseMemo
ReturnsA memoized functionA memoized computed value
EquivalentuseMemo(() => fn, deps)
Typical useStable callbacks for memoized children / effect depsExpensive calculations, stable object/array props
ArgumentThe function to cacheA 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 count is in the dependencies, the function still changes whenever count changes — so the memoization buys you nothing in that case.

Warning: Reach for useCallback to fix a measured performance problem, not preemptively. Profile with the React DevTools Profiler before adding it.

Best Practices

  • Use useCallback only when the callback flows into React.memo children, custom hooks, or dependency arrays of useEffect/useMemo.
  • List every reactive value the callback reads in the dependency array; trust exhaustive-deps over manual judgment.
  • Prefer functional state updaters to shrink dependency arrays and keep callbacks stable.
  • Remember that useCallback is pointless if the child receiving the function is not memoized.
  • Combine it with useMemo for objects/arrays and React.memo for components to make memoization actually effective.
  • Don’t optimize blindly — measure first, then memoize the hot paths.
Last updated June 14, 2026
Was this helpful?