memo, useMemo & useCallback
โฑ๏ธ ~3-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐ React.memo = caching a printed document. If the original file hasn't changed, reprint is unnecessary. useMemo = caching a calculation. useCallback = caching a function reference so dependent caches aren't invalidated.
React.memo
Cache a printed document
Wraps a component. Skips re-render if props are shallowly equal to previous render.
Best for: Slow-to-render components that receive stable props
useMemo
Cache a calculation
Memoizes any value returned from a function. Re-computes only when deps change.
Best for: Expensive computations: filter/sort/transform of large arrays
useCallback
Cache a function reference
Memoizes a function. Prevents memo'd children from re-rendering when a callback prop changes.
Best for: Stable callbacks passed to React.memo'd children
The pairing rule
React.memo + useCallback are a pair. useCallback is only valuable if the function is passed to a React.memo component. Without memo, the child re-renders anyway โ useCallback provides no benefit, only overhead.
Interactive Sandbox
โ Move something, see it react instantly.Pattern
Parent re-renders โ ALL children re-render, even if their props didn't change. Each parent state update cascades down the entire subtree.
| 1 | // Every time Parent re-renders, ChildA and ChildB re-render too |
| 2 | function Parent() { |
| 3 | const [count, setCount] = useState(0); |
| 4 | return ( |
| 5 | <div> |
| 6 | <button onClick={() => setCount(c => c + 1)}>+</button> |
| 7 | <ChildA value="static" /> {/* re-renders on every count change */} |
| 8 | <ChildB value="also static" /> {/* same */} |
| 9 | </div> |
| 10 | ); |
| 11 | } |
| 12 | ย |
| 13 | function ChildA({ value }: { value: string }) { |
| 14 | // This logs on every parent click โ even though value never changes |
| 15 | console.log('ChildA rendering'); |
| 16 | return <div>{value}</div>; |
| 17 | } |
Challenge
Explore all 5 patterns. Understand when memo helps and when it hurts.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: What is shallow comparison and why does it matter for memo?
| 1 | // Shallow comparison: compares by reference, not deep value |
| 2 | // Primitives: compared by value (OK) |
| 3 | prevProps.count === 42 โ |
| 4 | ย |
| 5 | // Objects: compared by reference (tricky) |
| 6 | prevProps.user === nextProps.user // true only if SAME object |
| 7 | ย |
| 8 | // Bug: parent creates new object each render |
| 9 | function Parent() { |
| 10 | // { id: 1 } creates a NEW object reference every render |
| 11 | return <MemoChild config={{ id: 1 }} />; // memo never skips! |
| 12 | } |
| 13 | ย |
| 14 | // Fix: stable reference with useMemo |
| 15 | const config = useMemo(() => ({ id: 1 }), []); |
| 16 | return <MemoChild config={config} />; |
Q: When does React.memo hurt performance?
When the comparison is more expensive than the render. A React.memo'd component that receives frequently-changing props runs both the comparison AND the render on every update โ strictly worse than no memo. Profile before adding memo.
Q: Why is `() => doSomething()` passed as prop problematic?
| 1 | // Arrow function in JSX = new reference on every render |
| 2 | <MemoChild onSave={() => save(formData)} /> |
| 3 | // โ MemoChild.memo comparison: prevProps.onSave !== nextProps.onSave |
| 4 | // โ always true โ memo never skips โ waste of React.memo |
| 5 | ย |
| 6 | // Fix: useCallback stabilizes the reference |
| 7 | const handleSave = useCallback(() => save(formData), [formData]); |
| 8 | <MemoChild onSave={handleSave} /> |
The Deep Dive
โ Spec refs, engine internals, the minutiae.React Compiler โ automatic memoization
React 19's Compiler analyzes component code at build time and automatically adds React.memo, useMemo, and useCallback where they are provably safe. Components that currently need manual memoization will be handled automatically. The compiler is opt-in via a Babel/SWC plugin and requires components to follow React's rules (no mutation, pure renders).
React DevTools Profiler โ how to find slow renders
| 1 | // 1. Open React DevTools โ Profiler tab |
| 2 | // 2. Click Record, interact with your app, click Stop |
| 3 | // 3. Flamegraph shows each component's render time |
| 4 | // 4. "Ranked" chart shows slowest components first |
| 5 | ย |
| 6 | // Target: any component >16ms in the critical path |
| 7 | // is a candidate for memoization |
| 8 | ย |
| 9 | // 5. Check "Why did this render?" (DevTools setting) |
| 10 | // โ shows which prop changed to trigger the re-render |
| 11 | // โ if it was a new function/object: stabilize with useCallback/useMemo |
Structural sharing in state updates
When you update a deeply nested object, Immer (used by Redux Toolkit) creates a new root reference but preserves unchanged subtrees by reference. This means React.memo'd components that receive unchanged subtrees correctly skip re-rendering โ Immer's structural sharing makes shallow comparison effective even for deep data.
Interview Questions
โ Real questions from real interviews โ with answers.memo caches a component; useMemo caches a value; useCallback caches a function reference.
Arrow functions in JSX create a new reference on every render, making shallow prop comparison always fail.
When props change frequently โ memo adds comparison cost on top of render cost, making it strictly worse.
Wrap the computation in useMemo with the raw data and filter/sort params as deps.
The compiler automates memoization โ manual memo/useMemo/useCallback become largely unnecessary.
Immer preserves unchanged subtree references, so memo'd children receiving unmodified subtrees correctly skip re-rendering.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.You add React.memo to a component and profile again โ render time is unchanged. The most likely cause is: