useMemo, useCallback, memo and the React Compiler
Referential equality is why memoization exists. Learn when useMemo, useCallback and React.memo help and when they are useless, how memo compares props, and what the React Compiler now does for you.
Comes up in: React, Recruiter screen
Learn#
Aarav wraps a heavy chart in React.memo, but the React Profiler shows it still renders on every keystroke. He passes options={{ color: "blue" }}.
Referential equality means two values are the same object, not merely equal-looking. { color: "blue" } === { color: "blue" } is false. memo skips a render only when every prop is the same as last time. A new object or function each render defeats it:
import { GlobalRegistrator } from "@happy-dom/global-registrator";
GlobalRegistrator.register(); // a DOM for Node; in a browser you already have one
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const { createElement: h, memo, useMemo, useState, act } = await import("react");
const { createRoot } = await import("react-dom/client");
const renders = { memoObject: 0, memoStable: 0 };
const Chart = memo(function Chart({ options, name }) { renders[name]++; return null; });
let bump;
function Dashboard() {
const [tick, setTick] = useState(0);
bump = () => setTick((t) => t + 1);
const stable = useMemo(() => ({ color: "blue" }), []);
return h("div", null,
h(Chart, { name: "memoObject", options: { color: "blue" } }), // new object every render
h(Chart, { name: "memoStable", options: stable })); // same object every render
}
const root = createRoot(document.createElement("div"));
await act(() => root.render(h(Dashboard)));
for (let i = 0; i < 3; i++) await act(() => bump());
console.log("memo + new object each render:", renders.memoObject, "renders");
console.log("memo + useMemo'd object:", renders.memoStable, "render");Output (verified)
memo + new object each render: 4 renders memo + useMemo'd object: 1 render
Memoization helps in three cases: an expensive calculation, props for a memo child, and dependencies of an effect. Elsewhere it only adds code.
State
- options (render 1) = object #1
Dashboard renders. It creates options = { color: "blue" }, a brand-new object, and passes it to the memoized Chart.
Read the steps as text
- Dashboard renders. It creates options = { color: "blue" }, a brand-new object, and passes it to the memoized Chart.
- Next render, the literal creates object #2. It looks the same, but it is a different object.
- memo compares each prop with Object.is. options changed, so Chart renders anyway: memo did nothing.
- useMemo(() => ({ color: "blue" }), []) returns object #1 every time, so memo can skip Chart.
Predict#
Predict first
A component calls useCallback for a click handler and passes it to a plain <button onClick={…}>. Does this save work?
Only memoized components (or effect dependencies) benefit from a stable function. A plain <button> is updated by React’s diff anyway. Wrapping every handler in useCallback is a common habit that adds code without benefit.
Interview angle#
Say it like this — about 30 seconds
“The useMemo hook caches a value, and useCallback caches a function. They help when the work is expensive, when the value goes to a memo child, or when it is an effect dependency. They are useless on cheap values, or on props for plain DOM elements. React.memo skips a render when the props are shallowly equal. The React Compiler is now stable. It adds this memoization for you, if your code follows the rules of React.”
Follow-up What is the difference between useMemo and useCallback? When are they useless?
Follow-up What does the React Compiler do? Do we still need useMemo?
Follow-up Can useMemo be used to guarantee a value is never recomputed?
Practice#
Guided
Write the shallow comparison memo uses and a small useMemo-like cache.
Done when
shallowEqualcompares own keys withObject.is, not deeply.- The memo cache recomputes only when dependencies change.
NaNdependencies count as unchanged, likeObject.is.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
How does React.memo compare props?
Shallowly, each prop with Object.is.
What is useCallback(fn, deps) equivalent to?
useMemo(() => fn, deps).
Name the three cases where memoization helps.
Expensive calculations, props to memo children, effect dependencies.
What does the React Compiler need from your code?
That it follows the rules of React: pure renders and hook rules.
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
Objects, arrays and functions created during render are new each time. Primitives such as numbers and strings compare by value, so they never break memo.
Check yourself: Is { a: 1 } === { a: 1 }?
No. They are two different objects.
How it works underneathSenior rounds
memo(Component, arePropsEqual) accepts a custom comparison, but a wrong one causes stale UI. Children passed as JSX are new elements every render, so memo on a component with children rarely helps Source: React: memo.Check yourself: Can you give memo your own comparison?
Yes, a second argument arePropsEqual(prev, next).
Edge cases and trapsWhere people slip
A missing dependency makes useMemo return a value built from old inputs. Let the exhaustive-deps lint rule list them. Memoizing costs memory and a comparison every render.
Check yourself: Why can a wrong dependency list cause a bug with useMemo?
The cached value keeps using old inputs.
In productionStaff-level answers
Check yourself: Which version of the React Compiler is stable?
1.0 (babel-plugin-react-compiler).
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-11-01. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- What is the difference between
useMemoanduseCallback? When are they useless? - What does the React Compiler do? Do we still need
useMemo?