TypeScript React useMemo: Typing Memoized Values
useMemo memoizes the result of an expensive computation and only recalculates it when dependencies change. In TypeScript, you annotate the return type of the computation, and TypeScript ensures that the memoized value matches what you expect.
In production applications, I've seen useMemo misused to memoize trivial operations, adding overhead without benefit. This article focuses on the right patterns and when memoization actually matters for performance.
Basic useMemo with Type Annotations
When you wrap a computation in useMemo, annotate the return type of the callback function:
interface User {
id: number;
name: string;
email: string;
}
interface ExpensiveResult {
userCount: number;
emailDomains: Set<string>;
}
function UserStats({ users }: { users: User[] }) {
// Expensive computation: extract unique domains
const stats: ExpensiveResult = React.useMemo(() => {
console.log('Computing user stats...');
const domains = new Set<string>();
users.forEach((user) => {
const domain = user.email.split('@')[1];
domains.add(domain);
});
return {
userCount: users.length,
emailDomains: domains,
};
}, [users]);
return (
<div>
<p>Total users: {stats.userCount}</p>
<p>Unique domains: {stats.emailDomains.size}</p>
</div>
);
}
Here, the callback returns an ExpensiveResult object. TypeScript infers that stats has the same type. The computation runs only when users changes, not on every render.
The key rule: if you memoize a computation, it should be genuinely expensive (e.g., sorting a large array, parsing JSON, running complex calculations). Memoizing simple operations (object literals, basic math) adds more overhead than it saves.
Memoizing Derived State
A common pattern is to memoize state derived from props:
interface ProductList {
id: string;
name: string;
price: number;
}
interface PriceStats {
min: number;
max: number;
average: number;
}
function Inventory({ products }: { products: ProductList[] }) {
const priceStats: PriceStats = React.useMemo(() => {
if (products.length === 0) {
return { min: 0, max: 0, average: 0 };
}
const prices = products.map((p) => p.price);
return {
min: Math.min(...prices),
max: Math.max(...prices),
average: prices.reduce((a, b) => a + b, 0) / prices.length,
};
}, [products]);
return (
<div>
<p>Min: {priceStats.min}</p>
<p>Max: {priceStats.max}</p>
<p>Average: {priceStats.average}</p>
</div>
);
}
This pattern is useful when you need to transform props before passing them to a memoized child. The parent recalculates only when products changes, not on every render.
Memoizing Object and Array References
A subtle issue: object and array literals are recreated every render, which breaks React.memo comparisons. Use useMemo to preserve the reference:
interface ConfigValue {
theme: 'light' | 'dark';
fontSize: number;
language: 'en' | 'es';
}
interface SettingsProps {
config: ConfigValue;
}
const Settings = React.memo(({ config }: SettingsProps) => (
<div>
<p>Theme: {config.theme}</p>
<p>Font size: {config.fontSize}</p>
</div>
));
function App() {
const [count, setCount] = React.useState(0);
// Bad: creates a new object every render, so Settings re-renders unnecessarily
const configBad: ConfigValue = {
theme: 'light',
fontSize: 16,
language: 'en',
};
// Good: preserves the object reference unless dependencies change
const configGood: ConfigValue = React.useMemo(
() => ({
theme: 'light',
fontSize: 16,
language: 'en',
}),
[] // Config is constant, so this never recalculates
);
return (
<div>
<Settings config={configGood} />
<button onClick={() => setCount(count + 1)}>Re-render</button>
</div>
);
}
Without useMemo, even though the config hasn't changed, it's a new object on every render, and React.memo sees a new prop and re-renders Settings. This is a common source of unnecessary re-renders in large apps.
Memoizing Array Transformations
When you filter or map an array, the new array is always a new reference:
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoListProps {
todos: Todo[];
}
const TodoList = React.memo(({ todos }: TodoListProps) => (
<ul>
{todos.map((todo) => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</li>
))}
</ul>
));
function App() {
const [todos, setTodos] = React.useState<Todo[]>([
{ id: 1, text: 'Learn TypeScript', completed: false },
{ id: 2, text: 'Build React app', completed: true },
]);
const [filter, setFilter] = React.useState<'all' | 'active' | 'completed'>('all');
const filteredTodos: Todo[] = React.useMemo(() => {
switch (filter) {
case 'active':
return todos.filter((t) => !t.completed);
case 'completed':
return todos.filter((t) => t.completed);
default:
return todos;
}
}, [todos, filter]);
return (
<div>
<TodoList todos={filteredTodos} />
<select value={filter} onChange={(e) => setFilter(e.currentTarget.value as any)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
</div>
);
}
The filteredTodos array is recalculated only when todos or filter changes. Without useMemo, the array would be filtered every render, even if nothing changed.
Generic useMemo in Custom Hooks
For custom hooks that return memoized values, use a generic type parameter:
function useMemoized<T>(
computation: () => T,
deps: React.DependencyList
): T {
return React.useMemo(computation, deps);
}
// Usage: TypeScript infers the type from the computation
interface ComplexResult {
data: string[];
count: number;
}
function MyComponent() {
const result: ComplexResult = useMemoized(
() => ({
data: ['a', 'b', 'c'],
count: 3,
}),
[]
);
return <div>{result.count} items</div>;
}
This wrapper preserves the computation's return type through the generic.
Combining useMemo with useCallback
When a memoized value is used in a useCallback, ensure dependencies are consistent:
interface SearchResult {
id: string;
title: string;
relevance: number;
}
function SearchWithMemo() {
const [query, setQuery] = React.useState('');
const [results, setResults] = React.useState<SearchResult[]>([]);
// Memoize the sorted results
const sortedResults: SearchResult[] = React.useMemo(
() => [...results].sort((a, b) => b.relevance - a.relevance),
[results]
);
// Callback that uses the memoized results
const topResult = React.useCallback((): SearchResult | null => {
return sortedResults[0] ?? null;
}, [sortedResults]);
return (
<div>
<input
value={query}
onChange={(e) => {
setQuery(e.currentTarget.value);
// Simulate search
setResults([
{ id: '1', title: 'Result 1', relevance: 0.9 },
{ id: '2', title: 'Result 2', relevance: 0.7 },
]);
}}
/>
<p>Top result: {topResult()?.title}</p>
</div>
);
}
Here, sortedResults is memoized, and the callback depends on it. Both are recalculated only when results changes.
Profiling and When to Use useMemo
Before adding useMemo, profile your app. React DevTools Profiler shows render times:
- Run the app normally and measure time.
- Add
useMemoand measure again. - If the improvement is less than a few milliseconds, remove it.
Memoization is worth the code complexity only if:
- The computation is genuinely expensive (takes >1 ms per render).
- The component re-renders frequently without dependency changes.
- The memoized value is passed to a memoized child component.
For most components, the cost of memoization overhead exceeds the benefit.
Key Takeaways
- Use
useMemoto memoize expensive computations (sorting, filtering, complex math) that would otherwise run on every render. - Memoize object and array references when they're passed to memoized child components to prevent unnecessary re-renders.
- Always include correct dependencies; missing dependencies cause stale values.
- Don't memoize trivial operations (simple objects, basic math)—the overhead costs more than it saves.
- Profile before and after adding
useMemoto confirm there's a measurable benefit.
Frequently Asked Questions
How do I know if a computation is expensive enough to memoize?
If the computation takes less than 1 millisecond, it's usually not worth memoizing. Use React DevTools Profiler to measure render times and identify bottlenecks.
What if I memoize too much—is there a downside?
Yes. Each useMemo call has overhead (comparing dependencies, storing the value). Too many can slow down renders. Use profiling to find the real bottlenecks.
Can useMemo replace useState for caching?
No. useMemo is cleared between renders if dependencies change, whereas state is preserved. Use state if you need the value to persist; use useMemo for derived/computed values.
What's the difference between useMemo and useCallback?
useCallback memoizes a function reference; useMemo memoizes a computed value. Both use the same dependency mechanism.