useCallback

Caches a function definition between re-renders so its identity stays stable until dependencies change.

Signature
const cachedFn = useCallback(fn, dependencies)
  • Added in React 16.8.0

Parameters

ParameterTypeDescription
$fnFunctionThe function value you want to cache. It can take any arguments and return any value. React returns it to you — it never calls it — so you decide when and whether to invoke it.
$dependenciesArray<any>The list of all reactive values referenced inside fn — props, state, and any variables and functions declared directly in the component body. The list must have a constant number of items and be written inline like [dep1, dep2, dep3]. React compares each dependency with its previous value using Object.is.

Return value

FunctionOn the initial render, useCallback returns the fn function you have passed. During subsequent renders, it returns the already stored fn from the last render (if the dependencies haven't changed), or the fn you passed during this render.

Errors

Throws an invalid-hook-call error if used outside a component or custom Hook body, or inconsistently across renders because it was placed inside a condition or loop.

Changelog

  • PHP 18.0.0React throws away the cached function if the component suspends during the initial mount, so consumers see a new identity after the data resolves.

useCallback returns the same function object across renders until one of its dependencies changes. It is useMemo specialised for functions: useCallback(fn, deps) is exactly useMemo(() => fn, deps). React never calls fn for you.

When to reach for it

  • The function is a prop of a component wrapped in memo. A fresh function every render is a changed prop, so the child re-renders regardless of its memoization.
  • The function is a dependency of useEffect, useMemo, or another useCallback. Without a stable identity the Effect re-runs — reconnecting a socket, refetching, resubscribing — on every render.
  • You are writing a custom Hook that returns functions. Callers cannot memoize what you hand them, so stabilise it for them.

What trips people up

Memoizing a handler passed to an ordinary component buys nothing: that child re-renders with its parent anyway. The optimisation only pays off when something downstream compares identities.

The other trap is dependency churn. If the callback closes over a piece of state you only ever write, list it as a dependency and you get a new function on every change. Use the updater form — setItems(prev => …) — and the dependency disappears, letting the array shrink to [].

Never omit a dependency to force stability; that freezes stale values into the closure.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Pass an onToggle or onDelete handler to rows wrapped in memo so a long list does not re-render when unrelated parent state changes.
  • Return a stable connect or submit function from a custom Hook that callers put in a useEffect dependency array.
  • Stop an Effect that subscribes to an external source from tearing down and resubscribing on every parent render.
  • Keep a debounced or throttled function from being recreated — and therefore losing its pending timer — between renders.

Frequently asked questions

When should I use useCallback instead of just declaring the function?

Only when the function's identity matters downstream — it is a prop of a memo-wrapped component, or an entry in a dependency array. For a handler on a plain DOM element or an ordinary child, declare the function inline.

Why does useCallback still return a new function every render?

Because a dependency changes every render. Objects, arrays, and functions created in the component body have new identities each time; stabilize them or drop them from the closure by using the setState updater form.

Is useCallback the same as useMemo?

useCallback(fn, deps) is shorthand for useMemo(() => fn, deps). useMemo caches the value a function returns, useCallback caches the function itself without calling it.

More examples

A stable function returned from a custom Hook

On mount the log reads "joined general". Typing in the draft input re-renders Chat but does not reconnect, because `connect` keeps its identity and the Effect's dependency is unchanged. Clicking "switch room" changes roomId, produces a new `connect`, tears down the old connection, and appends "joined random".

Example · jsx
import { useCallback, useEffect, useState } from "react";

function useChatRoom(roomId) {
  const [log, setLog] = useState([]);

  // Identity changes only when roomId changes.
  const connect = useCallback(() => {
    return { roomId, close: () => {} };
  }, [roomId]);

  useEffect(() => {
    const conn = connect();
    setLog((prev) => [...prev, `joined ${conn.roomId}`]);
    return () => conn.close();
  }, [connect]); // reconnects only when roomId changes

  return log;
}

export default function Chat() {
  const [room, setRoom] = useState("general");
  const [draft, setDraft] = useState("");
  const log = useChatRoom(room);

  return (
    <div>
      <input value={draft} onChange={(e) => setDraft(e.target.value)} />
      <button onClick={() => setRoom(room === "general" ? "random" : "general")}>
        switch room
      </button>
      <p>{log.join(" · ")}</p>
    </div>
  );
}

When useCallback alone is not enough

Panel renders "Save theme" and, after clicking, "Last saved: theme". The point is that memo compares every prop: with the commented-out object literal, Panel re-renders on each keystroke even though onSave is stable. Every non-primitive prop reaching a memo boundary has to be stabilised, not just the functions.

Example · jsx
import { memo, useCallback, useMemo, useState } from "react";

const Panel = memo(function Panel({ config, onSave }) {
  return <button onClick={() => onSave(config)}>Save {config.name}</button>;
});

export default function Settings() {
  const [name, setName] = useState("theme");
  const [saved, setSaved] = useState("nothing");

  // A stable handler is useless if a sibling prop churns:
  // const config = { name };  // new object every render → Panel re-renders

  const config = useMemo(() => ({ name }), [name]);
  const handleSave = useCallback((cfg) => setSaved(cfg.name), []);

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <Panel config={config} onSave={handleSave} />
      <p>Last saved: {saved}</p>
    </div>
  );
}

Discussion

  • Be the first to comment on this lesson.