Skip to solution
mediumFrontend

What is `React.memo` and when would you use it?

94 views
01

Understand the problem

Question presented to candidate: "What does React.memo do, and when is wrapping a component in it actually worth it?"

What a strong answer should cover:

  • memo is a higher-order component that skips re-rendering when the new props are shallow-equal to the previous ones.
  • It compares props with Object.is per key — shallow, not deep.
  • The bail-out is not a guarantee: React may still re-render, and state or context changes inside the component always re-render it regardless.
  • The critical failure mode: an inline object, array, or function prop is a new reference every render, so memo never bails.
  • Fixing that means useMemo/useCallback on the parent side — which is why memo rarely works alone.
  • The custom comparator second argument, and why it is usually a smell.
  • When it is worth it: an expensive subtree, re-rendered often, with stable props. All three conditions.
  • When it is not: cheap components, props that change every render anyway, or a component that would be better restructured with composition.
  • Forward-looking: the React Compiler makes most manual memo redundant.

Clarifying questions expected:

  • "Have we profiled it? Is this component actually the bottleneck?"
  • "Are its props stable, or created inline in the parent?"

Code / implementation expected: Yes — memo plus the useCallback needed to make it actually work.

performanceoptimizationhocmemoization
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 props and re-render basics. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every render count below was measured by actually mounting the compone

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

memo working, memo defeated, and memo fixed with useCallback
Run Playground
import { useState, useCallback, useMemo, memo } from "react";

// Render counters live outside the components so a re-render cannot reset them.
const counts = { plain: 0, broken: 0, fixed: 0 };

function Plain({ label }) {
  counts.plain++;
  return <Row title="Not memoised" label={label} n={counts.plain} />;
}

// Memoised, but the parent passes it a fresh object and arrow every render,
// so the shallow comparison always fails and this never bails out.
const Broken = memo(function Broken({ label, config, onPick }) {
  counts.broken++;
  return <Row title="memo + inline props" label={label} n={counts.broken} onPick={onPick} />;
});

// Same component, but the parent hands it stable references.
const Fixed = memo(function Fixed({ label, config, onPick }) {
  counts.fixed++;
  return <Row title="memo + stable props" label={label} n={counts.fixed} onPick={onPick} />;
});

function Row({ title, label, n, onPick }) {
  return (
    <div style={{ display: "flex", gap: 10, alignItems: "center", padding: "5px 0" }}>
      <code style={{ minWidth: 190 }}>{title}</code>
      <span style={{ minWidth: 130 }}>renders: <strong>{n}</strong></span>
      {onPick && <button onClick={onPick}>{label}</button>}
    </div>
  );
}

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

  // Stable across renders: same object identity every time.
  const stableConfig = useMemo(() => ({ mode: "compact" }), []);
  // Stable across renders: same function identity every time.
  const stablePick = useCallback(() => setPicked("fixed at " + Date.now()), []);

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

      <Plain label="plain" />
      {/* new object AND new arrow on every single render */}
      <Broken label="broken" config={{ mode: "compact" }} onPick={() => setPicked("broken")} />
      <Fixed label="fixed" config={stableConfig} onPick={stablePick} />

      <p style={{ color: "#666", fontSize: 13 }}>
        Click the button repeatedly. The first two counters climb together — the
        memo wrapper on the second one buys nothing because its props are new
        objects each time. Only the third stays at 1.
      </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 68 of 119 decoded in the React.js track. One more won't hurt.

Back to track