The Re-Render Problem
Every time a React component's state or props change, it re-renders. This is usually fine — React is fast. But when a component re-renders, it also re-creates all its functions and recalculates all its values.
For expensive calculations or functions passed to child components, this can cause unnecessary work. That's where useMemo and useCallback come in.
Key rule before you read further: Don't optimize until you have a measured performance problem. Premature memoization adds complexity and can actually hurt performance.
useMemo — Cache an Expensive Calculation
useMemo memoizes the result of a function. It only recalculates when its dependencies change.
Syntax:
const memoizedValue = useMemo(() => {
return expensiveCalculation(a, b);
}, [a, b]);
Without useMemo — recalculates on every render:
function ProductList({ products, filterText }) {
// This runs on EVERY render, even if products and filterText haven't changed
const filtered = products.filter(p =>
p.name.toLowerCase().includes(filterText.toLowerCase())
);
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
With useMemo — only recalculates when dependencies change:
function ProductList({ products, filterText }) {
const filtered = useMemo(
() => products.filter(p =>
p.name.toLowerCase().includes(filterText.toLowerCase())
),
[products, filterText] // only re-runs when these change
);
return <ul>{filtered.map(p => <li key={p.id}</li>)}</ul>;
}
When useMemo is worth it:
- The calculation is genuinely expensive (sorting/filtering large arrays, complex math)
- The component re-renders often with the same inputs
- You've confirmed the calculation is a bottleneck with profiling
When useMemo is NOT worth it:
- Simple operations like
a + bor basic string manipulation - Arrays/objects with fewer than ~100 items
- Components that rarely re-render
useCallback — Cache a Function Reference
useCallback memoizes a function itself (not its return value). It returns the same function instance between renders, as long as dependencies haven't changed.
Syntax:
const memoizedFn = useCallback(() => {
doSomething(a, b);
}, [a, b]);
Why does a stable function reference matter?
In JavaScript, every time a component renders, functions defined inside it are new objects:
function Component() {
const handleClick = () => console.log("clicked"); // new function every render
// handleClick === handleClick from last render → false
}
If you pass this function to a child component wrapped in React.memo, the child will re-render every time anyway, because the prop reference changed.
Without useCallback:
function Parent() {
const [count, setCount] = useState(0);
// New function every render → Child always re-renders
const handleSubmit = (data) => {
saveToServer(data);
};
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveChild onSubmit={handleSubmit} />
</>
);
}
With useCallback:
function Parent() {
const [count, setCount] = useState(0);
// Same function instance as long as no dependencies change
const handleSubmit = useCallback((data) => {
saveToServer(data);
}, []); // no dependencies — never changes
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<ExpensiveChild onSubmit={handleSubmit} />
</>
);
}
const ExpensiveChild = React.memo(({ onSubmit }) => {
console.log("ExpensiveChild rendered");
return <form onSubmit={onSubmit}>...</form>;
});
Now clicking the count button won't re-render ExpensiveChild.
React.memo — The Missing Piece
useCallback only makes sense alongside React.memo. Without React.memo, child components always re-render when the parent does — regardless of whether props changed.
// This component only re-renders when its props actually change
const Button = React.memo(function Button({ onClick, label }) {
console.log("Button rendered");
return <button onClick={onClick}>{label}</button>;
});
The trio works together: React.memo + useCallback + useMemo.
Practical Example: Search with Debounce
function SearchPage({ allProducts }) {
const [query, setQuery] = useState("");
const [sortOrder, setSortOrder] = useState("asc");
// Expensive: filter + sort on potentially thousands of items
const results = useMemo(() => {
const filtered = allProducts.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
);
return filtered.sort((a, b) =>
sortOrder === "asc"
? a.price - b.price
: b.price - a.price
);
}, [allProducts, query, sortOrder]);
const handleSort = useCallback((order) => {
setSortOrder(order);
}, []);
return (
<>
<input value={query} onChange={e => setQuery(e.target.value)} />
<SortControls onSort={handleSort} />
<ProductGrid products={results} />
</>
);
}
How to Identify Real Performance Problems
Before reaching for memoization, profile first:
- Open Chrome DevTools → Performance tab
- Click record, interact with your app, stop recording
- Look for long frames (red bars) and what's causing them
- Use React DevTools Profiler tab to see which components are re-rendering and how long they take
Only add useMemo / useCallback if profiling shows actual slowness.
Quick Decision Guide
Use useMemo when:
- Filtering or sorting large arrays (1000+ items)
- Complex calculations that depend on props/state
- Reference equality matters for a downstream
useMemooruseEffect
Use useCallback when:
- Passing callbacks to memoized child components (
React.memo) - Functions are dependencies of
useEffect
Use neither when:
- The operation is cheap
- The component renders rarely
- You haven't profiled and confirmed a bottleneck
Conclusion
useMemo and useCallback are surgical tools for specific performance problems — not default best practices. Write clear code first, measure second, and only then add memoization where the profiler tells you to. When used correctly, they eliminate wasted renders and keep complex UIs smooth.