React Memoization Hooks: Your Memory-Saving Superpower 🧠
The Kitchen Analogy
Imagine you’re a chef in a busy restaurant. Every time someone orders the same dish, would you:
A) Calculate the recipe from scratch, measure every ingredient again, and follow every step?
B) Remember you already made that exact dish 5 minutes ago and use the same recipe you figured out?
Smart chefs pick B! That’s exactly what memoization does in React. It remembers results so you don’t waste time doing the same work twice.
What is Memoization?
Memoization = Memory + Optimization
It’s like having a super smart notebook where you write down answers. When someone asks the same question, you just look it up instead of solving it again!
// Without memoization (solving every time)
function add(a, b) {
console.log("Calculating...");
return a + b;
}
add(2, 3); // "Calculating..." → 5
add(2, 3); // "Calculating..." → 5 (again!)
// With memoization (remembering)
// React checks: "Did I see 2+3 before?
// Yes! Here's 5!"
🎯 useMemo Hook
What is it?
useMemo is like having a smart assistant who remembers the answer to expensive calculations.
When to use it?
When you have a slow calculation that doesn’t need to run every time your component updates.
The Recipe
const result = useMemo(() => {
// Your slow calculation here
return expensiveWork(data);
}, [data]); // Only recalculate when data changes
Real Example: Filtering a Big List
function StudentList({ students, search }) {
// WITHOUT useMemo: Filters on EVERY render
// const filtered = students.filter(
// s => s.name.includes(search)
// );
// WITH useMemo: Only filters when needed
const filtered = useMemo(() => {
console.log("Filtering students...");
return students.filter(
s => s.name.includes(search)
);
}, [students, search]);
return (
<ul>
{filtered.map(s => <li>{s.name}</li>)}
</ul>
);
}
Visual Flow
graph TD A["Component Renders"] --> B{Did dependencies change?} B -->|Yes| C["Run calculation"] B -->|No| D["Return cached result"] C --> E["Cache new result"] E --> F["Use result"] D --> F
🎯 useCallback Hook
What is it?
useCallback is like giving your function a permanent ID card. React remembers it’s the same function, not a new one.
Why do functions need this?
Every time your component renders, it creates new functions. Even if they do the exact same thing!
function Parent() {
// This function is NEW every render!
const handleClick = () => {
console.log("Clicked!");
};
return <Child onClick={handleClick} />;
}
This causes Child to re-render unnecessarily because it thinks it got a “new” function!
The Fix: useCallback
function Parent() {
// Same function every render
const handleClick = useCallback(() => {
console.log("Clicked!");
}, []); // No dependencies = never changes
return <Child onClick={handleClick} />;
}
Real Example: Search Handler
function SearchBox({ onSearch }) {
const [query, setQuery] = useState("");
// Stable function reference
const handleSubmit = useCallback(() => {
onSearch(query);
}, [query, onSearch]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
<button onClick={handleSubmit}>
Search
</button>
</div>
);
}
Quick Comparison
| useMemo | useCallback |
|---|---|
| Remembers values | Remembers functions |
| Returns calculation result | Returns the function itself |
useMemo(() => value, []) |
useCallback(fn, []) |
✅ When to Memoize
The Traffic Light Rule 🚦
GREEN - DO memoize when:
- Expensive calculations (loops over 1000+ items)
- Passing callbacks to optimized children (React.memo)
- Used as dependency in other hooks
- Referential equality matters (object comparison)
RED - DON’T memoize when:
- Simple calculations (adding two numbers)
- Primitive values (strings, numbers, booleans)
- Not passed to children
- Component renders infrequently
Decision Flowchart
graph TD A["Should I memoize?"] --> B{Is it expensive?} B -->|Yes| C["useMemo"] B -->|No| D{Is it a function for child?} D -->|Yes| E{Is child memoized?} E -->|Yes| F["useCallback"] E -->|No| G["Probably not needed"] D -->|No| H[Don't memoize]
Good Examples
// ✅ GOOD: Expensive calculation
const sortedData = useMemo(() => {
return [...bigArray].sort((a, b) =>
a.score - b.score
);
}, [bigArray]);
// ✅ GOOD: Callback to memoized child
const handleDelete = useCallback((id) => {
setItems(prev =>
prev.filter(item => item.id !== id)
);
}, []);
// Memoized child benefits from stable callback
<MemoizedList onDelete={handleDelete} />
Bad Examples
// ❌ BAD: Simple calculation
const total = useMemo(() => {
return price + tax;
}, [price, tax]);
// Just write: const total = price + tax;
// ❌ BAD: Not passed anywhere
const format = useCallback((text) => {
return text.toUpperCase();
}, []);
// Just write: const format = (text) => ...
⚠️ Memoization Pitfalls
Pitfall #1: Forgetting Dependencies
// 🐛 BUG: Missing 'multiplier' in deps
const calculate = useMemo(() => {
return value * multiplier;
}, [value]); // Should include multiplier!
// ✅ FIX: Include all dependencies
const calculate = useMemo(() => {
return value * multiplier;
}, [value, multiplier]);
Pitfall #2: Over-Memoization
Memoization isn’t free! It has a cost:
// 🐛 WASTEFUL: Simple string
const greeting = useMemo(() => {
return `Hello, ${name}!`;
}, [name]);
// ✅ BETTER: Just compute it
const greeting = `Hello, ${name}!`;
Memoization costs:
- Memory to store cached value
- Comparison checks every render
- More code to maintain
Pitfall #3: Memoizing Without Memoized Children
// 🐛 POINTLESS: Child isn't memoized
const Parent = () => {
const handler = useCallback(() => {
doSomething();
}, []);
// RegularChild re-renders anyway!
return <RegularChild onClick={handler} />;
};
// ✅ ACTUALLY HELPS: Child is memoized
const MemoChild = React.memo(RegularChild);
const Parent = () => {
const handler = useCallback(() => {
doSomething();
}, []);
return <MemoChild onClick={handler} />;
};
Pitfall #4: Creating Objects in Dependencies
// 🐛 BUG: New object every render!
const result = useMemo(() => {
return processData(data);
}, [{ id: 1, name: "test" }]); // Always new!
// ✅ FIX: Use stable reference
const config = useMemo(() => ({
id: 1,
name: "test"
}), []);
const result = useMemo(() => {
return processData(data);
}, [config]);
Pitfall #5: Expensive Dependency Comparisons
// 🐛 PROBLEM: Large object comparison
const result = useMemo(() => {
return transform(hugeObject);
}, [hugeObject]); // Slow comparison!
// ✅ BETTER: Use specific properties
const result = useMemo(() => {
return transform(hugeObject);
}, [hugeObject.id, hugeObject.version]);
The Golden Rules Summary
- Measure first - Don’t guess, profile!
- Start without - Add memoization only when needed
- Include all deps - Trust the ESLint rules
- Pair with React.memo - useCallback alone isn’t enough
- Keep deps stable - Don’t create objects inline
Quick Reference
// useMemo: Cache expensive VALUES
const value = useMemo(() => {
return heavyCalculation(input);
}, [input]);
// useCallback: Cache FUNCTIONS
const fn = useCallback((arg) => {
doSomething(arg, dependency);
}, [dependency]);
// React.memo: Prevent re-renders
const MemoComponent = React.memo(Component);
Remember! 🎯
Premature optimization is the root of all evil. — Donald Knuth
Only memoize when you’ve measured a real performance problem. Your app is probably fast enough without it!
Happy coding! 🚀
