React Memoization Visualized: useMemo, useCallback, and React.memo
March 30, 2026
Memoization in React is not a magic switch. It is a trade-off between comparison cost, cache management cost, and re-render cost.
This article answers three practical questions:
- When does
React.memoactually skip rendering? - How does
useCallbackunlockReact.memoeffectiveness? - What does
useMemocache, and when does it invalidate?
REACT MEMOIZATION DECISION MAP
Problem
problem
React.memo
memo
useCallback
callback
useMemo
memoValue
Result
result
Parent re-renders propagate to children and expensive calculations.
Caching only works when dependency arrays are accurate and prop references are stable.
Apply memoization only to proven hot paths, not everywhere by default.
1. Root problem: why do components re-render too much?
In React, when a parent re-renders, children usually re-render with the tree.
This is not always bad, but it becomes expensive when:
- children are heavy,
- props are unchanged,
- or expensive derivations run repeatedly.
Memoization is how we skip unnecessary work in those scenarios.
2. React.memo: skip child render when props are unchanged
React.memo(Component) performs a shallow comparison of previous and next props.
If props are shallow-equal, React can skip rendering that component.
const UserCard = React.memo(function UserCard({ user }) {
return <div>{user.name}</div>;
});Important nuance:
React.memodoes not mean "never render again".- It only skips when that component's props are unchanged.
- If props are new object/function references each render, memoization often breaks.
2.1 What comparison does React actually use?
By default, React.memo does a shallow prop comparison with semantics close to Object.is per prop.
Quick model:
- Primitive props (
number,string,boolean,null,undefined) compare by value. - Object/array/function props compare by reference identity, not deep content.
So:
count={1}across renders is often considered unchanged.{a: 1}recreated each render is a new reference, so considered changed.() => doSomething()recreated each render is also a new function reference.
Typical memo-breaking pattern:
<Child config={{ pageSize: 20 }} onSave={() => save(id)} />Even if the logical values are "the same", child still re-renders because both config and onSave are new references.
2.2 How to stabilize object/function props
- Use
useMemofor object/array props. - Use
useCallbackfor function props.
const config = useMemo(() => ({ pageSize: 20 }), []);
const onSave = useCallback(() => save(id), [id]);
return <Child config={config} onSave={onSave} />;2.3 Custom comparator in React.memo (areEqual / isEqual)
React.memo accepts a custom comparator as the second argument:
const Child = React.memo(
function Child(props) {
return <div>{props.user.name}</div>;
},
(prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
},
);Important rule (easy to invert):
- Comparator returns
true=> React skips rendering. - Comparator returns
false=> React re-renders.
You may see names like areEqual, isEqual, or deep-compare helpers (e.g. lodash/isEqual), but use with caution:
- Deep compare can cost more CPU than the render you are trying to skip.
- Wrong comparator logic can produce stale UI.
- Ignoring function props in comparator can preserve stale closures.
Practical guidance:
- Stabilize references with
useMemo/useCallbackfirst. - Use custom comparators only for profiled hotspots.
- Keep comparator logic small and obviously correct.
3. useCallback: stabilize function prop identity
React.memo frequently fails because function props are recreated on every parent render.
const onSave = () => save(id); // new function each renderThen the child receives a new prop reference, shallow compare fails, and child re-renders.
useCallback stabilizes function identity based on dependencies:
const onSave = useCallback(() => save(id), [id]);USECALLBACK + REACT.MEMO IN PRACTICE
parent tick: 0
actions: 0
memo child with inline callback
Child render count: 1
memo child with stable callback
Child render count: 1
Parent re-renders change inline function identity, which breaks memo skip. useCallback keeps identity stable.
Quick checklist:
- Are you passing callbacks to memoized children?
- Does the parent re-render for unrelated state?
- Can callback identity be stabilized with
useCallback?
4. useMemo: cache expensive derived values
useMemo memoizes a computed value.
const filtered = useMemo(() => {
return products.filter((p) => p.name.includes(keyword));
}, [products, keyword]);It helps when:
- computation is non-trivial,
- dependencies change less frequently,
- the derived value is reused across renders.
USEMEMO CACHE VS RE-RENDER NOISE
Compute count: 1
React, Redux, Recoil, Relay, Remix
Compute count: 1
React, Redux, Recoil, Relay, Remix
Toggle unrelated state to see that useMemo keeps cached value until keyword dependency changes.
Common misunderstanding:
useMemois not automatically faster in every case.- It adds its own dependency tracking/caching overhead.
- Overusing it can reduce readability more than it helps performance.
5. Combining all three effectively
A common production pattern:
- Wrap heavy child in
React.memo. - Pass stable callbacks via
useCallback. - Build derived collections via
useMemo.
Example:
const Row = React.memo(function Row({ item, onSelect }) {
return <button onClick={() => onSelect(item.id)}>{item.name}</button>;
});
function ProductList({ products, keyword }) {
const filtered = useMemo(
() => products.filter((p) => p.name.includes(keyword)),
[products, keyword],
);
const handleSelect = useCallback((id: string) => {
console.log("select", id);
}, []);
return (
<div>
{filtered.map((item) => (
<Row key={item.id} item={item} onSelect={handleSelect} />
))}
</div>
);
}6. When NOT to memoize
- Tiny components with low render cost.
- No profiling evidence of a bottleneck.
- Dependencies change so often cache hit rate is near zero.
- Team readability/maintenance cost becomes too high.
Practical rule:
- Profile first.
- Optimize real hotspots.
- Measure again after optimization.
Wrap-up
React.memo, useCallback, and useMemo are powerful together when used deliberately.
Remember:
React.memooptimizes component render.useCallbackoptimizes function prop identity.useMemooptimizes computed values.
Performance work in React should start from measurements, not intuition.