Skip to solution
mediumFrontend

What is the difference between useMemo and useCallback?

750 views
01

Understand 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.

hooksperformanceoptimization
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes hooks and re-renders. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The recompute and identity counts in section 3 were measured across three re

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Recompute counts and function identities, with and without memoisation
Run Playground
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>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 49 of 119 decoded in the React.js track. One more won't hurt.

Back to track