Stop unnecessary re-renders in React. Learn exactly when useMemo and useCallback help performance — and when they're just adding complexity.
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 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:
When useMemo is NOT worth it:
a + b or basic string manipulationuseCallback 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.
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.
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} />
</>
);
}
Before reaching for memoization, profile first:
Only add useMemo / useCallback if profiling shows actual slowness.
Use useMemo when:
useMemo or useEffectUse useCallback when:
React.memo)useEffectUse neither when:
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.