Skip to the solution
MediumFrontend6 min read

What is the difference between useMemo and useCallback?

750 views

The problem

Question presented to candidate: "What is the difference between useMemo and useCallback, and when is each actually worth it?"

What a strong answer should cover:

  • useMemo(fn, deps) caches the result of calling fn. useCallback(fn, deps) caches the function itself.
  • They are the same mechanism: useCallback(fn, deps) is exactly useMemo(() => fn, deps).
  • Both compare dependencies with Object.is and recompute only when one changes.
  • Two distinct reasons to use either: avoiding an expensive computation, and preserving referential identity so a memoised child or a dependency array does not see a change.
  • The identity reason is by far the more common one in practice.
  • Neither is free: both add a dependency array to maintain and a comparison on every render. Applied everywhere they are a net loss.
  • They only pay off in specific conditions β€” a measured expensive computation, or a stable reference genuinely consumed by React.memo, a dependency array, or a context value.
  • The React Compiler automates this class of memoisation, making manual use largely redundant in compiled code.

Clarifying questions expected:

  • "Am I trying to avoid a computation, or preserve an identity?" β€” the two motivations lead to different hooks.
  • "Have we profiled? Is this actually the bottleneck?"

Code / implementation expected: Yes β€” both hooks, with visible recompute and identity counts.

  • hooks
  • performance
  • optimization

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 10 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactRecompute counts and function identities, with and without memoisation
import { useState, useMemo, useCallback, memo, useRef } from "react";

const stats = { computedMemo: 0, computedPlain: 0 };

// A memoised child, so function identity actually gets checked.
let childRenders = 0;
const Child = memo(function Child({ onPick, label }) {
  childRenders++;
  return (
    <p style={{ margin: "4px 0", fontSize: 14 }}>
      <code style={{ display: "inline-block", minWidth: 180 }}>{label}</code>
      child renders: <strong>{childRenders}</strong>{" "}
      <button onClick={() => onPick(1)}>pick</button>
    </p>
  );
});

const ITEMS = ["delta", "alpha", "charlie", "bravo"];

export default function App() {
  const [tick, setTick] = useState(0);
  const [picked, setPicked] = useState("none");

  // Identities seen across renders β€” a Set counts the distinct ones.
  const memoValues = useRef(new Set());
  const plainValues = useRef(new Set());
  const stableFns = useRef(new Set());
  const freshFns = useRef(new Set());

  // useMemo β€” caches the RESULT. Recomputes only when ITEMS changes (never).
  const sortedMemo = useMemo(() => {
    stats.computedMemo++;
    return ITEMS.toSorted();
  }, []);

  // No memo β€” recomputed and reallocated on every single render.
  stats.computedPlain++;
  const sortedPlain = ITEMS.toSorted();

  // useCallback β€” caches the FUNCTION. One identity for the component lifetime.
  const stablePick = useCallback((id) => setPicked("stable " + id), []);
  // Fresh arrow β€” a new identity every render, so memo on the child cannot help.
  const freshPick = (id) => setPicked("fresh " + id);

  memoValues.current.add(sortedMemo);
  plainValues.current.add(sortedPlain);
  stableFns.current.add(stablePick);
  freshFns.current.add(freshPick);

  const td = { padding: "3px 14px 3px 0", fontFamily: "ui-monospace, monospace", fontSize: 13 };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 540 }}>
      <p>
        <button onClick={() => setTick((t) => t + 1)}>re-render ({tick})</button>{" "}
        <span style={{ color: "#666", fontSize: 13 }}>picked: {picked}</span>
      </p>

      <table style={{ borderCollapse: "collapse", marginBottom: 12 }}>
        <tbody>
          <tr><td style={td}>useMemo β€” times computed</td><td style={{ ...td, color: "#161" }}>{stats.computedMemo}</td></tr>
          <tr><td style={td}>no memo β€” times computed</td><td style={{ ...td, color: "#a33" }}>{stats.computedPlain}</td></tr>
          <tr><td style={td}>useMemo β€” distinct values</td><td style={{ ...td, color: "#161" }}>{memoValues.current.size}</td></tr>
          <tr><td style={td}>no memo β€” distinct values</td><td style={{ ...td, color: "#a33" }}>{plainValues.current.size}</td></tr>
          <tr><td style={td}>useCallback β€” distinct fns</td><td style={{ ...td, color: "#161" }}>{stableFns.current.size}</td></tr>
          <tr><td style={td}>fresh arrow β€” distinct fns</td><td style={{ ...td, color: "#a33" }}>{freshFns.current.size}</td></tr>
        </tbody>
      </table>

      <Child onPick={stablePick} label="memo child + useCallback" />

      <p style={{ color: "#666", fontSize: 13 }}>
        Press re-render a few times. The memoised value and the stable function
        each stay at 1 distinct instance; the unmemoised ones climb with every
        render. The child stays at 1 render because its function prop never
        changes identity β€” swap in the fresh arrow and it would climb too.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 49 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

How do you handle routing in React applications?

Continue