Skip to the solution
MediumFrontend7 min read

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

94 views

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.

  • performance
  • optimization
  • hoc
  • memoization

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

7 min read · 9 sections

Try it in the playground

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

Reactmemo working, memo defeated, and memo fixed with useCallback
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>
  );
}

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 68 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

Error Boundaries with RSC — why can't Server Components catch errors?

Continue