Chapter 1: useCallback and useMemo
The Problem: Unnecessary Re-renders
Every time a component re-renders, all its code is re-executed:
function ProductList({ products }) {
// Recalculated on EVERY render, even if products hasn't changed
const sortedProducts = products.sort((a, b) => a.price - b.price);
// New function created on EVERY render
const handleClick = (id) => {
console.log('Clicked:', id);
};
return (
<ul>
{sortedProducts.map(p => (
<ProductItem key={p.id} product={p} onClick={handleClick} />
))}
</ul>
);
}
If products contains 1000 elements, the sort is done on every render, even if nothing has changed.
useMemo: Memoize a Value
useMemo memoizes the result of a calculation and only recalculates it when dependencies change.
Syntax
const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);
Example: Expensive Calculation
import { useMemo } from 'react';
function ProductList({ products, sortBy }) {
// Recalculated ONLY if products or sortBy changes
const sortedProducts = useMemo(() => {
console.log('Sorting...');
return [...products].sort((a, b) => {
if (sortBy === 'price') return a.price - b.price;
if (sortBy === 'name') return a.name.localeCompare(b.name);
return 0;
});
}, [products, sortBy]);
return (
<ul>
{sortedProducts.map(p => (
<li key={p.id}>{p.name} - {p.price}€</li>
))}
</ul>
);
}
Example: Filtering
function UserList({ users, searchTerm }) {
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]);
return (
<ul>
{filteredUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
useCallback: Memoize a Function
useCallback memoizes a function and only recreates it when dependencies change.
Syntax
const memoizedFn = useCallback(() => {
doSomething(a, b);
}, [a, b]);
Why Is This Useful?
In JavaScript, every time a function is defined, it's a new reference:
const fn1 = () => console.log('hello');
const fn2 = () => console.log('hello');
fn1 === fn2; // false! They are different functions
This causes problems with React.memo and useEffect dependencies.
Example with React.memo
// Memoized child component
const ProductItem = React.memo(function ProductItem({ product, onSelect }) {
console.log('ProductItem rendered:', product.name);
return (
<li onClick={() => onSelect(product.id)}>
{product.name}
</li>
);
});
// Parent component
function ProductList({ products }) {
const [selected, setSelected] = useState(null);
// WITHOUT useCallback: new function on every render
// -> ProductItem re-renders even if product hasn't changed
const handleSelect = (id) => {
setSelected(id);
};
// WITH useCallback: same function as long as dependencies don't change
const handleSelectMemo = useCallback((id) => {
setSelected(id);
}, []); // [] because setSelected is stable
return (
<ul>
{products.map(p => (
<ProductItem
key={p.id}
product={p}
onSelect={handleSelectMemo}
/>
))}
</ul>
);
}
Example with Dependencies
function SearchForm({ onSearch, category }) {
const [query, setQuery] = useState('');
// The function depends on category
const handleSubmit = useCallback(() => {
onSearch(query, category);
}, [onSearch, query, category]);
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button type="submit">Search</button>
</form>
);
}
React.memo: Memoize a Component
React.memo prevents a component from re-rendering if its props haven't changed.
const MyComponent = React.memo(function MyComponent({ name, onClick }) {
console.log('MyComponent rendered');
return <button onClick={onClick}>{name}</button>;
});
Important: React.memo compares props by reference. If a function is recreated on every render, the component will still re-render.
When to Use useMemo?
YES:
- Expensive calculations (sorting, filtering large lists)
- Creating objects passed as props to memoized components
- Calculations whose result is used in multiple places
NO:
- Simple calculations (addition, concatenation)
- Components that re-render anyway
- "Just in case" by default
When to Use useCallback?
YES:
- Functions passed to memoized child components (React.memo)
- Functions in useEffect dependencies
- Expensive event handlers
NO:
- Functions used only locally
- Child components not memoized
- "For optimization" by default
Comparison
| Hook | Memoizes | Recalculates if |
|---|---|---|
| useMemo | A value | Dependencies change |
| useCallback | A function | Dependencies change |
| React.memo | A component | Props change |
Pitfall: Incorrect Dependencies
// PROBLEM: items is missing from dependencies
const getTotal = useCallback(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, []); // items should be in the array!
// CORRECT
const getTotal = useCallback(() => {
return items.reduce((sum, item) => sum + item.price, 0);
}, [items]);
ESLint with the eslint-plugin-react-hooks plugin will warn you about missing dependencies.
Exercises
Exercise 1: Optimized Filtered List
Create a product list with a search field. Optimize to not re-filter if the search hasn't changed.
Solution:
function OptimizedProductList({ products }) {
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState('name');
const filteredAndSorted = useMemo(() => {
console.log('Filtering and sorting...');
return products
.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'price') return a.price - b.price;
return 0;
});
}, [products, search, sortBy]);
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search..."
/>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">By name</option>
<option value="price">By price</option>
</select>
<ul>
{filteredAndSorted.map(p => (
<li key={p.id}>{p.name} - {p.price}€</li>
))}
</ul>
</div>
);
}
Exercise 2: Stable Callback
Create a parent component with multiple child buttons. Optimize so buttons don't re-render unnecessarily.
Solution:
const Button = React.memo(function Button({ id, onClick, children }) {
console.log('Button rendered:', id);
return <button onClick={() => onClick(id)}>{children}</button>;
});
function ButtonGroup() {
const [clicked, setClicked] = useState(null);
const handleClick = useCallback((id) => {
setClicked(id);
console.log('Button clicked:', id);
}, []);
return (
<div>
<p>Last click: {clicked}</p>
<Button id="a" onClick={handleClick}>Button A</Button>
<Button id="b" onClick={handleClick}>Button B</Button>
<Button id="c" onClick={handleClick}>Button C</Button>
</div>
);
}
Key Points to Remember
- useMemo memoizes a calculated value
- useCallback memoizes a function
- React.memo memoizes a component
- Don't optimize prematurely - measure first
- Complete dependencies - always list all used values