useMemo and useCallback are often touted as essential performance optimizations in React, but most of the time, they do absolutely nothing for your application’s speed.
Let’s see useMemo in action. Imagine we have a component that renders a list of items, and we want to perform a computationally expensive operation on each item before displaying it.
import React, { useState, useMemo } from 'react';
const expensiveOperation = (item) => {
console.log(`Performing expensive operation on ${item.id}`);
// Simulate a heavy computation
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
return `${item.name} - Processed`;
};
const ItemList = ({ items }) => {
const [filter, setFilter] = useState('');
const filteredAndProcessedItems = useMemo(() => {
console.log('Recalculating filtered and processed items');
return items
.filter(item => item.name.includes(filter))
.map(item => ({
...item,
processedName: expensiveOperation(item)
}));
}, [items, filter]); // Dependencies: items and filter
return (
<div>
<input
type="text"
placeholder="Filter items"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<ul>
{filteredAndProcessedItems.map(item => (
<li key={item.id}>{item.processedName}</li>
))}
</ul>
</div>
);
};
export default ItemList;
In this example, useMemo is wrapping the filtering and mapping logic. The expensiveOperation function is only called when items or filter actually change. If the ItemList component re-renders due to some unrelated state change (e.g., a parent component’s state changing), the console.log('Recalculating filtered and processed items') will not appear, and expensiveOperation won’t run again.
The core problem useMemo and useCallback aim to solve is unnecessary re-computation or re-creation of values and functions during React re-renders.
When a React component re-renders, JavaScript re-executes the component function. This means any variables declared directly inside the component are recreated, and any functions defined inline are redefined. If these variables or functions are passed as props to child components, React’s reconciliation algorithm might see them as "new" props, even if their values or behavior are identical. This can lead to unnecessary re-renders of those child components, especially if they are memoized with React.memo.
useMemo memoizes a computed value. It takes a function that computes the value and an array of dependencies. The function is only re-executed if one of the dependencies has changed since the last render.
useCallback memoizes a function itself. It also takes a function and an array of dependencies. It returns the same function instance as long as the dependencies haven’t changed. This is crucial when passing callback functions down to memoized child components.
The mental model for when to use them is simple: if a value or function is expensive to compute/create, or if it’s a dependency for another memoized value/function, and it doesn’t change on every render, then memoize it.
"Expensive" doesn’t just mean complex mathematical operations. It can also mean:
- Data transformations: Filtering, sorting, mapping large arrays.
- Object/Array creation: Creating new object or array literals on every render that are then passed as props.
- Function re-creation: Passing inline functions as event handlers to
React.memo-wrapped children.
The trap most developers fall into is premature optimization. They wrap everything in useMemo or useCallback "just in case." This adds overhead (memory for storing the memoized value, comparison of dependencies) without any performance gain if the computation wasn’t actually expensive or if the dependencies change frequently anyway. React’s reconciliation is already quite efficient, and for most small-to-medium applications, the default behavior is perfectly fine.
A common scenario where useCallback is genuinely useful is with event handlers passed to React.memo-wrapped components that depend on component state or props. For example, if you have a Button component wrapped in React.memo and you pass an onClick handler that references stateVariable from the parent, without useCallback, a parent re-render (even if stateVariable didn’t change) would create a new onClick function, causing the Button to re-render. Using useCallback for that onClick handler prevents this.
The one thing most people don’t know is that useMemo and useCallback themselves have a small overhead. They need to store the previous value/function and compare the dependencies on each render. If your computation is trivial (e.g., adding two numbers, returning a string literal) or if your dependencies change on every single render, you’re often better off not using them, as the overhead of the hook itself can outweigh any perceived benefit.
The next place you’ll likely run into trouble is understanding how React.memo interacts with these hooks, and when you might need to combine them for maximum effect.