Memoization cheatsheet.
The default: don’t memoize
Default React behavior — every state change re-renders subtree. That’s fine 95% of the time. Don’t optimize prematurely.
Reach for memoization when you’ve measured a slow render and know why.
React Compiler (2025+)
The React Compiler auto-memoizes for you. If you’re using it, skip explicit memo/useMemo/useCallback.
// .babelrc / next.config.js
{
"plugins": ["babel-plugin-react-compiler"]
}
Compiler reads your code and inserts memoization where beneficial.
React.memo
const Item = React.memo(function Item({ name, count }: Props) {
return <li>{name}: {count}</li>;
});
Skips re-render if props are shallow-equal to previous.
Custom compare:
const Item = React.memo(Item, (prev, next) => prev.id === next.id);
useMemo
const sorted = useMemo(() => items.slice().sort(cmp), [items]);
Cache expensive computation. Trade memory for CPU.
// Cache object identity (referenced as dep elsewhere)
const config = useMemo(() => ({ apiUrl, timeout }), [apiUrl, timeout]);
useCallback
const onClick = useCallback((id: number) => doIt(id), [doIt]);
Cache function identity. Only meaningful if passed to:
- A memoized component (
React.memo) - An effect/hook deps array
Otherwise it’s noise.
When memo helps
- Child component is expensive to render.
- Child is in a tight loop (large list).
- Function or object prop changes identity unnecessarily.
When it doesn’t:
- Child is cheap (most components are).
- Props change every render anyway.
- New object/array literal as prop — memo can’t help.
Measuring
import { Profiler } from "react";
<Profiler id="List" onRender={(id, phase, dur) => console.log(id, phase, dur)}>
<List />
</Profiler>
Or React DevTools Profiler tab — record, see hot components.
Lifting work out of render
// BAD: expensive in render
function List({ items }: { items: Item[] }) {
const sorted = items.slice().sort(cmp); // every render
return <ul>{sorted.map(...)}</ul>;
}
// GOOD: useMemo
const sorted = useMemo(() => items.slice().sort(cmp), [items]);
List virtualization
For huge lists, render only visible:
import { useVirtualizer } from "@tanstack/react-virtual";
const v = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
});
return (
<div ref={parentRef} style={{ height: 400, overflow: "auto" }}>
<div style={{ height: v.getTotalSize() }}>
{v.getVirtualItems().map((row) => (
<div key={row.key} style={{ position: "absolute", top: row.start }}>
{items[row.index].name}
</div>
))}
</div>
</div>
);
10k items, only ~20 DOM nodes.
Keys and stable identity
Reused identity matters:
{items.map((it) => (
<Item key={it.id} item={it} /> // stable key → React reuses
))}
Reorder with index keys destroys child state.
Avoid creating arrays/objects inline
// BAD: new array every render
<Child items={data.filter(p)} />
// BETTER
const filtered = useMemo(() => data.filter(p), [data]);
<Child items={filtered} />
If Child is React.memo, this matters; otherwise it’s fine.
Splitting components
Sometimes the fix is structural:
function Parent() {
const [count, setCount] = useState(0);
return (
<>
<Counter count={count} setCount={setCount} />
<ExpensiveTree /> // doesn't depend on count
</>
);
}
If ExpensiveTree doesn’t depend on count, move state down:
function Parent() {
return (
<>
<Counter /> // owns its own count
<ExpensiveTree />
</>
);
}
Context value memoization
// Provider:
const value = useMemo(() => ({ user, login, logout }), [user]);
Or split context (see Cheatsheet 05).
Common mistakes
- Memoizing everything “just in case” — overhead may exceed savings.
useCallbackfor handlers used only in JSX — adds work, no benefit.React.memo+ always-changing prop — never bails out.- Forgetting
useMemodep — stale data. - Wrapping cheap components in memo — slower than not.
Read this next
If you want my virtualization + heavy-list patterns, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .