Skip to main content

React Hooks — useCallback & useMemo

Difficulty: 🟡 Medium | Frequency: ⭐⭐⭐⭐⭐

Core Distinction

  • useCallback — memoizes a function reference
  • useMemo — memoizes a computed value
// useCallback: returns the function itself
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);

// useMemo: returns the result of calling the function
const sortedList = useMemo(() => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

// useCallback(fn, deps) === useMemo(() => fn, deps)

Why They Exist: Referential Equality

JavaScript compares functions and objects by reference, not value. Every render creates new references:

function Parent() {
const handleClick = () => console.log('clicked'); // NEW function each render
const options = { theme: 'dark' }; // NEW object each render

return <Child onClick={handleClick} options={options} />;
// Child re-renders even if nothing relevant changed!
}

useCallback and useMemo stabilize references so memoized children can bail out.

useCallback

// ❌ Without: new function every render → breaks React.memo on child
const handleDelete = (id) => {
setItems(prev => prev.filter(item => item.id !== id));
};

// ✅ With: stable reference → React.memo works
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []); // No deps because we use functional update

When to Use

  • Passing callbacks to memoized child components (React.memo)
  • In dependency arrays of other hooks

Dependency Array Patterns

// ✅ Functional update avoids state dependency
const increment = useCallback(() => {
setCount(prev => prev + 1);
}, []); // Not: [count]

// ✅ Use refs for values you need but don't want as deps
const latestData = useRef(data);
useEffect(() => { latestData.current = data; }, [data]);

const handleSubmit = useCallback(() => {
process(latestData.current); // Read from ref
}, []); // Stable reference

useMemo

// ❌ Expensive calculation runs on every render
const sortedUsers = users
.filter(u => u.active)
.sort((a, b) => a.name.localeCompare(b.name));

// ✅ Only recalculates when users changes
const sortedUsers = useMemo(() => {
return users
.filter(u => u.active)
.sort((a, b) => a.name.localeCompare(b.name));
}, [users]);

Common Use Cases

// 1. Expensive calculations
const total = useMemo(() =>
items.reduce((sum, item) => sum + item.price * item.qty, 0),
[items]
);

// 2. Stable object references (for context or props)
const contextValue = useMemo(() => ({
user,
login,
logout
}), [user]);

// 3. Derived data from props
const filtered = useMemo(() =>
products.filter(p => p.category === selectedCategory),
[products, selectedCategory]
);

React.memo + useCallback/useMemo = No Unnecessary Re-renders

// Child only re-renders if its props change
const ExpensiveList = React.memo(function ExpensiveList({ items, onDelete }) {
console.log('ExpensiveList rendered');
return items.map(item => (
<div key={item.id}>
{item.name}
<button onClick={() => onDelete(item.id)}>Delete</button>
</div>
));
});

function Parent({ items }) {
const [count, setCount] = useState(0);

// Stable reference — ExpensiveList won't re-render when count changes
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(i => i.id !== id));
}, []);

return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveList items={items} onDelete={handleDelete} />
</>
);
}

The Memoization Paradox — Don't Over-Optimize

Memoization adds memory overhead and comparison cost. It's harmful when:

  • The component is cheap to render
  • Props change every render anyway (negates memoization)
  • You add it "just in case" without profiling

Only memoize when profiling confirms a bottleneck (render > 16ms).

// ❌ Unnecessary: cheap computation
const doubled = useMemo(() => count * 2, [count]);
const doubled = count * 2; // ✅ Just compute it

// ✅ Necessary: 5000-item sort on every render
const sorted = useMemo(() => largeArray.sort(compareFunc), [largeArray]);

Real-World Impact

Dashboard search feature before optimization: 800–1200ms input lag. After strategic useCallback + useMemo: 16–32ms (50× improvement).

Root cause: Parent re-rendered on every keystroke, creating new function references, breaking memoization on all chart components, triggering 247 component renders per keystroke.


Content from Frontend-Master-Prep-Series03-react