When to Use useMemo and useCallback
How and when to use React's useMemo and useCallback hooks for performance optimization, and when they add unnecessary overhead.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
useMemo caches a computed value so React reuses it across renders unless its dependencies change. useCallback caches a function reference for the same purpose. Both hooks prevent unnecessary re-renders and redundant calculations. But they have their own cost — storing the cached value and comparing dependencies on every render. Using them on values that are cheap to compute or functions that aren’t passed to memoized children actually makes things slower.
When to Use
- Expensive computations: filtering a list of 10,000 items, parsing large JSON, complex math
- Stable references for memoized children: passing callbacks to
React.memocomponents that would otherwise re-render on every parent render - Stable dependencies for other hooks: a value used in a
useEffectdependency array that shouldn’t trigger the effect on every render - Context value stabilization: preventing all context consumers from re-rendering when the provider’s state changes
When NOT to Use
- Cheap computations: simple arithmetic, string concatenation, small array operations — the hook overhead exceeds the savings
- Functions passed only to non-memoized children: if the child doesn’t use
React.memo, a new function reference doesn’t cause extra renders - Primitive values:
useMemo(() => 42, [])— primitives are compared by value, not reference - Every variable and function: wrapping everything in hooks is cargo-cult programming, not optimization
Solution
Basic useMemo for expensive computation
import { useMemo, useState } from "react";
function ProductList({ products }) {
const [query, setQuery] = useState("");
const [sort, setSort] = useState("name");
const filteredSorted = useMemo(() => {
const filtered = products.filter((p) =>
p.name.toLowerCase().includes(query.toLowerCase())
);
return filtered.sort((a, b) => {
if (sort === "name") return a.name.localeCompare(b.name);
if (sort === "price") return a.price - b.price;
return 0;
});
}, [products, query, sort]);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<select value={sort} onChange={(e) => setSort(e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
<ul>
{filteredSorted.map((p) => (
<li key={p.id}>{p.name} — ${p.price}</li>
))}
</ul>
</div>
);
}
Without useMemo, every keystroke in the search input re-filters and re-sorts the entire product list even if products hasn’t changed.
useCallback for stable function references
import { useCallback, useState, memo } from "react";
const ExpensiveChild = memo(function ExpensiveChild({ onClick, id }) {
console.log("ExpensiveChild rendered");
return <button onClick={() => onClick(id)}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
const handleClick = useCallback((id) => {
console.log("Clicked item", id);
}, []);
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveChild onClick={handleClick} id={1} />
</div>
);
}
Without useCallback, every render of Parent (triggered by typing in the input or incrementing count) creates a new handleClick reference, which breaks memo on ExpensiveChild and causes it to re-render.
When useCallback is unnecessary
function Parent() {
const [text, setText] = useState("");
// This child is NOT memoized — it re-renders on every parent render anyway
const handleClick = (id) => {
console.log("Clicked", id);
};
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={() => handleClick(1)}>Click</button>
</div>
);
}
Wrapping handleClick in useCallback here adds overhead with zero benefit — the button isn’t a memoized component.
useMemo for context value stabilization
import { createContext, useContext, useMemo, useState } from "react";
const UserContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("light");
const value = useMemo(
() => ({ user, setUser, theme, setTheme }),
[user, theme]
);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
function UserProfile() {
const { user } = useContext(UserContext);
return <div>{user?.name}</div>;
}
Without useMemo, every provider render creates a new value object, causing all consumers to re-render — even if only theme changed and UserProfile only uses user.
Measuring with React Profiler
import { Profiler } from "react";
function onRender(id, phase, actualDuration) {
console.log(`${id} ${phase} took ${actualDuration}ms`);
}
function App() {
return (
<Profiler id="ProductList" onRender={onRender}>
<ProductList products={largeProductList} />
</Profiler>
);
}
Use the Profiler to measure actual render times before and after adding useMemo. If the duration doesn’t improve, remove the hook.
useMemo with expensive object creation
function Chart({ data }) {
const chartConfig = useMemo(() => {
return {
scales: {
x: { type: "time" },
y: { min: Math.min(...data.map((d) => d.value)) },
},
plugins: {
tooltip: {
callbacks: {
label: (ctx) => `${ctx.dataset.label}: ${ctx.parsed.y}`,
},
},
},
};
}, [data]);
return <canvas ref={(c) => drawChart(c, chartConfig)} />;
}
useCallback with useEffect dependency
function SearchResults({ query, onSearch }) {
const [results, setResults] = useState([]);
const fetchResults = useCallback(async () => {
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
setResults(data);
}, [query]);
useEffect(() => {
fetchResults();
}, [fetchResults]);
return <ResultList results={results} onSearch={onSearch} />;
}
Without useCallback, fetchResults is a new function on every render, causing useEffect to re-run on every render — an infinite loop.
Variants
Using useMemo for debounced values
function SearchInput() {
const [input, setInput] = useState("");
const debounced = useMemo(() => {
let timeout;
return (value) => {
clearTimeout(timeout);
timeout = setTimeout(() => setInput(value), 300);
};
}, []);
return <input onChange={(e) => debounced(e.target.value)} />;
}
Custom hook combining useMemo and useCallback
function useFilteredData(data, filterFn) {
const stableFilter = useCallback(filterFn, [filterFn]);
const filtered = useMemo(() => data.filter(stableFilter), [data, stableFilter]);
const sorted = useMemo(() => [...filtered].sort((a, b) => a.id - b.id), [filtered]);
return sorted;
}
Best Practices
-
For a deeper guide, see Virtualize Long Lists with react-window.
-
Measure before optimizing — use the React Profiler to identify actual bottlenecks
-
Apply
useMemoto computations that take more than 1-2ms — cheaper ones aren’t worth the hook overhead -
Apply
useCallbackonly when the function is passed to aReact.memochild or used as auseEffectdependency -
Keep dependency arrays complete — omitting a dependency causes stale closures and bugs
-
Don’t memoize primitive values (strings, numbers, booleans) — they’re compared by value
-
Use
React.memoon children before usinguseCallback— memoizing a callback for a non-memoized child is wasted
Common Mistakes
- Wrapping everything in hooks:
useMemo(() => a + b, [a, b])for simple addition is slower thanconst sum = a + b - Empty dependency arrays with changing values:
useMemo(() => compute(x), [])—xis captured once and never updates - Using useMemo for side effects:
useMemois for pure computations. UseuseEffectfor side effects. - Not memoizing context values: a new object literal in a context provider causes all consumers to re-render
- Memoizing the wrong thing: memoizing the result of a cheap operation while ignoring the expensive one
FAQ
Does useMemo guarantee the cached value is reused?
No. React may discard cached values to free memory. useMemo is a hint, not a guarantee. Don’t rely on it for correctness — only for performance.
Should I always use React.memo on components?
No. React.memo adds a shallow comparison on every render. If the component’s props change on every render anyway (new objects, new arrays), memo never prevents a re-render and the comparison is wasted overhead.
What is the overhead of useMemo?
On every render, React compares each dependency with Object.is. For a hook with 3 dependencies, that’s 3 comparisons plus the cost of storing the cached value. For cheap computations (under 1ms), this overhead exceeds the savings.
Can I use useMemo for async operations?
No. useMemo is synchronous. For async, use useEffect with state, or a library like @tanstack/react-query or SWR.
How do I know if a computation is expensive enough to memoize?
Use console.time and console.timeEnd around the computation, or use the React Profiler. If a render takes more than 16ms (one frame at 60fps), look for expensive computations to memoize.
Related Resources
Virtualize Long Lists with react-window
How to render large lists efficiently in React using react-window for DOM virtualization, including fixed and variable height rows and grid layouts.
RecipeForm Validation with react-hook-form and Zod
How to build type-safe forms in React using react-hook-form with Zod schema validation, including nested fields, async validation, and dynamic fields.
RecipeContainer Queries for Component Responsiveness
How to use CSS container queries for component-level responsive layouts that adapt to their container size instead of the viewport.
RecipeDark Mode with prefers-color-scheme and CSS Variables
How to implement dark mode using CSS prefers-color-scheme media query, CSS custom properties, and manual toggle with localStorage persistence.
RecipeReactive State Management with Svelte Stores
How to manage reactive state in Svelte using writable, readable, derived stores, and custom stores with contract-based updates.
RecipeExhaustive Type Checking with Discriminated Unions
How to use TypeScript discriminated unions for exhaustive type checking, ensuring all cases are handled at compile time with never type assertions.
RecipeBuild Reusable Utility Types with Generics
How to create reusable TypeScript utility types using conditional types, mapped types, template literals, and generic constraints for type-safe APIs.