On this page · sections
Memoization & React Compiler
Memoization skips redundant work in the render cycle — React.memo at the component layer, useMemo and useCallback at the hook layer. The React Compiler automates much of this at build time via compile-time dependency analysis.
| API / Tool | What it caches | Layer | Skips |
|---|---|---|---|
React.memo | Component render decision | Component | Child re-render if props equal |
useMemo | Computed value | Hook | Recomputation; stable ref |
useCallback | Function reference | Hook | New function identity |
| React Compiler | Values + functions where safe | Build time | Manual memo boilerplate |
React.memo
Definition
React.memo wraps a component and skips re-rendering when props are shallow-equal to the previous render.
'use client';
import { memo, useState } from 'react';
type RowProps = { id: string; label: string };
const ExpensiveRow = memo(function ExpensiveRow({ id, label }: RowProps) {
return (
<tr>
<td>{id}</td>
<td>{label}</td>
</tr>
);
});
function Table({ rows }: { rows: RowProps[] }) {
const [filter, setFilter] = useState('');
return (
<>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
<table>
<tbody>
{rows.map((row) => (
<ExpensiveRow key={row.id} id={row.id} label={row.label} />
))}
</tbody>
</table>
</>
);
}Typing in filter re-renders Table but memoized rows skip — props unchanged. Custom compare is possible but easy to get wrong:
const Row = memo(
function Row({ item }: { item: Item }) {
return <li>{item.label}</li>;
},
(prev, next) => prev.item.id === next.item.id,
);memo guards the child; the parent still renders. Context consumers bypass props comparison entirely.
Interview: Inline objects, arrays, and arrow functions create new references every render and defeat
memo.
useMemo
Definition
useMemo(compute, deps) caches a computed value between renders. When deps are unchanged, it returns the same reference.
'use client';
import { memo, useMemo, useState } from 'react';
const TagSummary = memo(function TagSummary({
summary,
}: {
summary: Record<string, number>;
}) {
return <pre>{JSON.stringify(summary)}</pre>;
});
function Inventory({ items }: { items: Item[] }) {
const [query, setQuery] = useState('');
const filtered = useMemo(() => {
const q = query.toLowerCase();
return items.filter((item) =>
item.tags.some((t) => t.toLowerCase().includes(q)),
);
}, [items, query]);
const tagCounts = useMemo(() => {
const counts: Record<string, number> = {};
for (const item of filtered) {
for (const tag of item.tags) {
counts[tag] = (counts[tag] ?? 0) + 1;
}
}
return counts;
}, [filtered]);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<TagSummary summary={tagCounts} />
</>
);
}Use for expensive pure calculations and stable object identity passed to memo children. useMemo must be pure — no side effects.
Interview:
useMemocaches a value.React.memoskips a component render. Different layers.
useCallback
Definition
useCallback(fn, deps) returns the same function reference when deps are unchanged.
'use client';
import { memo, useCallback, useState } from 'react';
const ToolbarButton = memo(function ToolbarButton({
label,
onPress,
}: {
label: string;
onPress: () => void;
}) {
return (
<button type="button" onClick={onPress}>
{label}
</button>
);
});
function Toolbar({ onSave }: { onSave: (payload: string) => void }) {
const [draft, setDraft] = useState('');
const handleSave = useCallback(() => onSave(draft), [draft, onSave]);
const handleClear = useCallback(() => setDraft(''), []);
return (
<div>
<input value={draft} onChange={(e) => setDraft(e.target.value)} />
<ToolbarButton label="Save" onPress={handleSave} />
<ToolbarButton label="Clear" onPress={handleClear} />
</div>
);
}Matters when passing callbacks to memo children or listing callbacks in effect deps. dispatch from useReducer is stable — no useCallback needed.
Interview:
useCallbackdoes not make the function faster — only its reference identity is stable.
What Defeats Memoization
Definition
function BrokenParent() {
const [n, setN] = useState(0);
const onClick = useCallback(() => setN((v) => v + 1), []);
// new config every render — memo always fails
return <Widget config={{ color: 'red' }} onClick={onClick} />;
}Common defeaters:
Inline object literal → <Child style={{ color: 'red' }} />
Inline array → <Child items={[a, b]} />
Inline arrow function → <Child onClick={() => doWork()} />
Unstable context value → memo on consumer is wastedFix order before reaching for memo:
1. Colocate state down
2. Split context
3. Pass expensive subtrees as children
4. Stabilize values with useMemo / useCallback
5. Then memo — where profiler implicatesInterview: If Profiler shows memo never bails out, remove it — compare tax with zero skip.
React Compiler
Definition
The React Compiler is a Babel plugin that analyzes component control flow at build time and inserts automatic memoization — caching values and stabilizing functions where dependencies are provably safe.
'use client';
import { useState } from 'react';
function SearchList({ items }: { items: string[] }) {
const [query, setQuery] = useState('');
const filtered = items.filter((item) =>
item.toLowerCase().includes(query.toLowerCase()),
);
const handleSelect = (id: string) => {
console.log('selected', id);
};
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>
{filtered.map((item) => (
<li key={item} onClick={() => handleSelect(item)}>{item}</li>
))}
</ul>
</div>
);
}
// With compiler: stable handleSelect + filtered cache when deps unchangedSay automatic memoization with compile-time dependency analysis — not “it makes React faster magically.”
Rules of React for compiler safety
Components and hooks must be pure during render:
// ❌ Impure render — Date.now() drives UI
function BadClock() {
return <span>{Date.now()}</span>;
}
// ✅ State + effect
function GoodClock() {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, []);
return <span>{now}</span>;
}Pure render → same props/state → same output; no fetch in render
Hooks → top level only; no conditional hooks
Immutable state → mutations hide dependency changes
Refs → imperative escape; do not drive UI from ref mutations aloneInterview: Compiler targets client components and hooks. Server Components do not re-render — compiler focus is client interactivity.
Compiler vs Manual Memo
Definition
Remove redundant manual memo once compiler is on and ESLint is clean. Keep manual memo when you need semantic guarantees the compiler cannot prove.
// Keep — expensive derived data with explicit contract
function useStableSnapshot(data: BigObject) {
return useMemo(() => ({ count: Object.keys(data.nodes).length }), [data]);
}
// Delete after compiler — ceremonial useCallback
function HandMemoButton({ onClick, label }: { onClick: () => void; label: string }) {
const stableClick = useCallback(onClick, [onClick]);
return <button onClick={stableClick}>{label}</button>;
}Remove → redundant useCallback/useMemo mirroring compiler output
Keep → custom equality, library interop, documented public API stability
Still required → virtualization, code splitting, children composition, Context splitOpt out surgically with "use no memo" per module or component. Use eslint-plugin-react-compiler in CI.
Interview: Compiler does not fix Context broadcast — split contexts or use selectors. Compiler does not fix lists without keys.
When Memoization Hurts
Definition
False security → memo with unstable props = compare tax + no skip
Stale closures → useCallback missing dep → old state in handler
Garbage retention → large memoized objects stay alive while deps unchanged
Premature memo → profile before addingEvent handlers are often cheap to recreate — useCallback only when measured need exists. memo on a context consumer is wasted — context updates bypass props comparison.
Interview: Architecture fixes beat memo carpet — colocate state, split context, virtualize lists.
Senior Interview Summary
What to say
React.memoskips child renders on shallow-equal props.useMemocaches values;useCallbackstabilizes function references. The React Compiler inserts automatic memoization at build time for pure components — remove redundant manual memo in compiled paths, keep it for library interop and custom equality.
Best mental model
React.memo → "Skip rendering this component."
useMemo → "Remember this computed value."
useCallback → "Remember this function reference."
Compiler → "Auto memo where analysis is safe."Practical rule
Profile first → memo without evidence is overhead
Inline object defeats memo → fix parent, not band-aid useMemo
Context consumer + memo → wasted; split context instead
Expensive pure derive → useMemo
Callback to memo child → useCallback
Scroll jank on 10k rows → virtualize, not more memo
Enable compiler per package → ESLint gate → leaf packages → INP metrics