09 — useMemo & useCallback
What They Do and Why They Exist
memoization_basics.js
import { useMemo, useCallback } from 'react'
// useMemo: memoizes a VALUE. Returns the same reference if deps haven't changed.
const sortedList = useMemo(() => items.sort((a, b) => a.name.localeCompare(b.name)), [items])
// If items is the same reference → returns the cached sorted array
// If items changes → recomputes and returns a new array
// useCallback: memoizes a FUNCTION reference.
const handleClick = useCallback(() => doSomething(id), [id])
// If id is the same → returns the same function reference
// If id changes → returns a new function
// EQUIVALENCE: useCallback(fn, deps) === useMemo(() => fn, deps)
// useCallback is just shorthand for useMemo with a function return value.
// WHY THEY EXIST: referential equality.
// On every render, inline objects/functions create NEW references:
// {} !== {} (same content, different object)
// () => {} !== () => {} (same logic, different function instance)
// These new references break:
// 1. React.memo child components (shallow prop comparison fails → re-renders)
// 2. useEffect/useCallback dependency arrays (Object.is fails → effect re-runs)
// 3. Context value comparison (new object → all consumers re-render)
// useMemo/useCallback stabilize references so these comparisons succeed.
When to Use: The Decision Matrix
when_to_use.js
// USE MEMOIZATION WHEN:
// 1. The value/function is passed to a React.memo'd child
// 2. The value/function is in a useEffect/useCallback/useMemo dependency array
// 3. The value is an expensive computation (sorting/filtering large datasets)
// 4. The value is a context provider value (Ch 7)
// DON'T USE MEMOIZATION WHEN:
// 1. The computation is cheap (arithmetic, simple string ops, small array maps)
// — the memoization overhead (comparison + storage) costs more than the computation
// 2. The child isn't React.memo'd — it re-renders on parent render regardless
// 3. The value/function is only used in the same component (no dependency array)
// ANTI-PATTERN: wrapping everything "just in case"
function BadComponent({ data }) {
const value = useMemo(() => data + 1, [data]) // addition is FREE — no memo needed
const handler = useCallback(() => console.log('click'), []) // used only here, no memo needed
return <div onClick={handler}>{value}</div>
// Neither the child (plain div) nor any dependency array needs stable references.
// The useMemo/useCallback overhead is pure waste.
}
React.memo + useCallback + useMemo: The Production Pattern
production_memoization.js
import { memo, useMemo, useCallback } from 'react'
// React.memo wraps a component and prevents re-renders when props are shallow-equal
const ExpensiveRow = memo(function Row({ item, onSelect, isSelected }) {
// This component does expensive work (or renders many DOM nodes).
// React.memo does a shallow comparison of props: Object.is(prevProp, nextProp)
// If ALL props are the same reference → SKIPS re-render entirely.
return (
<tr className={isSelected ? 'selected' : ''} onClick={() => onSelect(item.id)}>
<td>{item.name}</td>
<td>{item.price}</td>
</tr>
)
})
function ProductTable({ items, selectedId }) {
// 1. Memoize the sorted/filtered data so the array reference is stable
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
)
// 2. Memoize the handler so the function reference is stable
const handleSelect = useCallback((id) => {
console.log('Selected:', id)
}, []) // no deps → stable forever (unless it needs state/props)
// 3. React.memo on Row + stable props → Row only re-renders when its item changes
return (
<table>
<tbody>
{sortedItems.map(item => (
<ExpensiveRow
key={item.id}
item={item}
onSelect={handleSelect}
isSelected={item.id === selectedId}
/>
))}
</tbody>
</table>
)
}
// Without ALL THREE (memo + useMemo + useCallback), the optimization breaks:
// - No React.memo on Row → Row re-renders on every parent render regardless
// - No useMemo on sortedItems → new array every render → Row sees new item ref
// - No useCallback on handleSelect → new function every render → prop changes → Row re-renders
The Cost of Memoization
memoization_cost.js
// useMemo/useCallback are NOT free — they have overhead:
// 1. Comparison cost: React runs Object.is on every dep on every render
// 2. Storage cost: the memoized value + deps array are stored in fiber
// 3. GC pressure: cached values stay in memory until deps change
// RULE OF THUMB: only memoize when the cost of NOT memoizing > the cost of memoizing
// - NOT memoizing cost: unnecessary re-render of an expensive component
// - Memoizing cost: O(deps) comparison + memory on every render
// When memoization HURTS:
function CheapList({ items }) {
// Items is a small array (10 items). The sort is trivial.
const sorted = useMemo(() => items.sort(), [items]) // memo overhead > sort cost
// If items changes frequently, the memo never hits cache → pure overhead.
// Just compute inline: const sorted = items.sort()
return <div>{sorted.map(i => <span key={i}>{i}</span>)}</div>
}
// When memoization HELPS:
function BigList({ items }) {
// 10,000 items. Sort takes 50ms. Parent re-renders frequently (e.g., search box).
const sorted = useMemo(() => items.sort((a, b) => a.score - b.score), [items])
// If items doesn't change but parent re-renders → memo saves 50ms per render.
return <div>{sorted.map(i => <Row key={i.id} data={i} />)}</div>
}
Common Pitfall: Inline Objects Break memo
inline_object_pitfall.js
// ANTI-PATTERN: React.memo'd child receives a new inline object every render
const StyledButton = memo(function StyledButton({ label, style }) {
return <button style={style}>{label}</button>
})
function Toolbar({ theme }) {
return <StyledButton
label="Save"
style={{ color: theme.color, padding: '8px' }} // ← NEW OBJECT every render
/>
// React.memo compares prevStyle === nextStyle → {} !== {} → ALWAYS re-renders.
// The memo is completely defeated by the inline object.
}
// FIX: memoize the style object
function Toolbar({ theme }) {
const buttonStyle = useMemo(
() => ({ color: theme.color, padding: '8px' }),
[theme.color] // only re-create when theme.color changes
)
return <StyledButton label="Save" style={buttonStyle} />
// Now the style reference is stable → React.memo works.
}
// OR: hoist to module scope if truly constant
const STATIC_STYLE = { color: 'blue', padding: '8px' }
💡 Tips & Tricks
tips.js
// [Performance] Measure BEFORE optimizing. Use React DevTools Profiler to find
// which components re-render unnecessarily. Don't sprinkle useMemo/useCallback
// "just in case" — most components don't need them.
// [Idiom] useCallback(fn, []) is equivalent to useRef(fn).current if fn doesn't
// need any deps — but useCallback is more idiomatic and readable.
// [Debug] If React.memo doesn't seem to prevent re-renders, check for:
// 1. Inline objects: style={{...}}, config={{...}} → new ref every render
// 2. Inline functions: onClick={() => ...} → new ref every render
// 3. Children prop: <Memo>text</Memo> → children string is stable, but
// <Memo><Child/></Memo> → new JSX element every render
// [Performance] For lists, the highest-leverage optimization is usually
// virtualization (react-window/react-virtual), not memoization. Memoization
// helps each row avoid unnecessary re-renders; virtualization avoids rendering
// rows that aren't visible at all. See Chapter 20.
// [Idiom] useMemo for expensive computations: sort/filter of large datasets,
// complex object construction, parsing. Not for arithmetic or small operations.
⚠️ Edge Cases & Gotchas
edge_cases.js
// [Gotcha] React.memo does a SHALLOW comparison — Object.is on each prop.
// It does NOT deeply compare objects. { a: 1 } !== { a: 1 } → memo fails.
// You must stabilize references with useMemo/useCallback for memo to work.
// [Gotcha] React.memo with a custom comparison function:
// memo(Component, (prevProps, nextProps) => prevProps.data.id === nextProps.data.id)
// Returning true = props are equal → skip re-render. Returning false = re-render.
// This is the OPPOSITE of what you might expect (true = skip, not true = render).
// [Gotcha] useMemo doesn't guarantee the value won't be recomputed — React may
// "forget" memoized values to free memory. Don't rely on useMemo for side effects
// or as a guarantee that a computation runs exactly once. Use useRef + useState
// for "run once" semantics.
// [Gotcha] useCallback with a function that closes over state will capture the
// state at the time the callback was created. If the state changes but isn't in
// deps, the callback is stale. Always include all referenced state/props in deps.
// [Gotcha] Children passed as JSX (<Memo><Child/></Memo>) create a new element
// every render, which can defeat React.memo if children is a prop. Use
// useMemo for the children element or render children outside the memo'd boundary.
🧠 Spot the Bug
A React.memo'd child component re-renders on every parent render, even though the developer expected it to be stable:
spot_the_bug.js
const ExpensiveChart = memo(function ExpensiveChart({ data, options, onSelect }) {
return <Chart data={data} options={options} onSelect={onSelect} />
})
function Dashboard({ rawData, theme }) {
return (
<ExpensiveChart
data={rawData}
options={{ theme, animated: true }} // ← new object every render
onSelect={(id) => console.log(id)} // ← new function every render
/>
)
}
Answer
Both options and onSelect are inline literals — new object/function references on every render. React.memo does a shallow comparison (Object.is on each prop), and since { theme, animated: true } !== { theme, animated: true } and () => console.log(id) !== () => console.log(id), the memo always fails and the chart re-renders every time.
Fix:
function Dashboard({ rawData, theme }) {
const options = useMemo(() => ({ theme, animated: true }), [theme])
const handleSelect = useCallback((id) => console.log(id), [])
return <ExpensiveChart data={rawData} options={options} onSelect={handleSelect} />
}
Now options only changes when theme changes, and handleSelect is stable forever — React.memo succeeds and the chart only re-renders when rawData or theme actually changes.
Key Takeaways
key_takeaways.js
// 1. useMemo memoizes a VALUE, useCallback memoizes a FUNCTION reference.
// Both return the same cached result if deps haven't changed (Object.is).
// useCallback(fn, deps) === useMemo(() => fn, deps).
// 2. They exist for REFERENTIAL EQUALITY — preventing new object/function refs
// from breaking React.memo, dependency arrays, and context value comparison.
// 3. Use when: value/function goes to a React.memo'd child, is in a dep array,
// is an expensive computation, or is a context value.
// Don't use when: computation is cheap, child isn't memo'd, no dep array needs it.
// 4. React.memo + useMemo + useCallback must ALL be present for the optimization
// to work. Missing any one: memo on child (no memo → always re-renders),
// unstable data ref, unstable handler ref → memo comparison fails.
// 5. Memoization has overhead (comparison + storage). Measure with the Profiler
// before optimizing. For lists, virtualization (Ch 20) usually beats memoization.