β‘ useMemo and useCallback
Welcome to one of React's most powerful performance optimization tools! You've learned about useState for managing state, useEffect for side effects, and useRef for persisting values - but what happens when your components start doing expensive calculations on every render, or when child components re-render unnecessarily? That's where useMemo and useCallback come in! Think of useMemo as a "smart cache" that remembers the result of expensive calculations, and useCallback as a "function memory" that keeps your functions stable across renders. These hooks are like performance boosters for your React app - but with great power comes great responsibility! Let's learn when to use them (and when NOT to). β‘
π― Learning Objectives
By the end of this lesson, you will be able to:
- Understand what memoization is and how it works
- Use useMemo to optimize expensive computations
- Use useCallback to memoize function references
- Identify when to use (and when NOT to use) these hooks
- Type memoized values and callbacks with TypeScript
- Understand React's rendering behavior and optimization
- Combine useMemo/useCallback with other hooks effectively
- Profile and measure performance improvements
- Avoid common memoization pitfalls and anti-patterns
Estimated Time: 60-75 minutes
Project: Build optimized data tables, search filters, and complex calculations
π In This Lesson
π― Understanding Performance in React
Before we dive into useMemo and useCallback, let's understand how React renders components and why performance optimization matters.
How React Renders Components
Every time a component's state or props change, React re-renders that component and all its children. This is usually fine - React is fast! But sometimes, it can cause performance issues.
When Does Performance Matter?
π‘ Performance Matters When:
- Expensive calculations: Filtering/sorting large lists, complex computations
- Frequent re-renders: Components that update many times per second
- Large component trees: Parent re-renders causing cascade of child re-renders
- User experience: UI feels sluggish, animations stutter, inputs lag
- Data-heavy operations: Processing thousands of items, complex transformations
The Performance Trap
// β οΈ This runs EVERY render, even if data hasn't changed!
function ProductList({ products }: { products: Product[] }) {
console.log('Rendering ProductList');
// This expensive calculation runs on EVERY render
const sortedProducts = products
.slice()
.sort((a, b) => b.rating - a.rating);
const averagePrice = products.reduce((sum, p) => sum + p.price, 0) / products.length;
return (
<div>
<p>Average Price: ${averagePrice.toFixed(2)}</p>
{sortedProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// If parent re-renders for ANY reason, these calculations run again!
// Even if products array hasn't changed!
β οΈ The Problem
Every time the parent component re-renders (maybe because of unrelated state), this component recalculates everything:
- Sorts the entire products array
- Calculates average price
- Creates new function references
- Even if the products data is identical!
React's Default Behavior
| What Happens | Consequence | When It's a Problem |
|---|---|---|
| Parent re-renders | All children re-render | Deep component trees |
| Component re-renders | All code runs again | Expensive calculations |
| Functions recreated | New references each render | Props to memoized children |
| Objects/arrays recreated | Reference equality fails | Dependency arrays, memo |
β The Good News
React provides two hooks to solve these problems:
- useMemo: Memoizes (caches) the result of expensive calculations
- useCallback: Memoizes (caches) function references
These hooks tell React: "Only recalculate/recreate when dependencies actually change!"
π§ What is Memoization?
Memoization is a programming technique where you cache (remember) the result of expensive operations and return the cached result when the same inputs occur again.
π Definition
Memoization: An optimization technique that stores the results of expensive function calls and returns the cached result when the same inputs occur again, avoiding redundant calculations.
Real-World Analogy
π The Pizza Restaurant Analogy
Imagine a pizza restaurant where making a pizza takes 20 minutes:
Without Memoization:
- Customer orders a Margherita pizza β Cook makes it (20 min)
- 5 minutes later, another customer orders a Margherita β Cook makes it again (20 min)
- Another customer orders the same β Cook makes it again (20 min)
- Total time wasted: 60 minutes for the same recipe!
With Memoization:
- First Margherita order β Cook makes it and saves the recipe result (20 min)
- Second Margherita order β Cook checks cache, serves immediately! (instant)
- Third Margherita order β Serve from cache again! (instant)
- Different pizza (Pepperoni) β Cache miss, make fresh and cache it
How Memoization Works
Simple Memoization Example
// Manual memoization (conceptual)
const cache = new Map();
function expensiveCalculation(n: number): number {
// Check if we've calculated this before
if (cache.has(n)) {
console.log('Returning cached result');
return cache.get(n)!;
}
// Not in cache, calculate it
console.log('Calculating...');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += n;
}
// Store in cache for next time
cache.set(n, result);
return result;
}
// First call - calculates (slow)
expensiveCalculation(5); // Logs: "Calculating..."
// Second call with same input - returns cached result (fast!)
expensiveCalculation(5); // Logs: "Returning cached result"
// Different input - calculates again
expensiveCalculation(10); // Logs: "Calculating..."
Memoization in React
React's useMemo and useCallback do this automatically for you! They check if dependencies changed, and only recalculate when needed.
import { useMemo } from 'react';
function MyComponent({ data }: { data: number[] }) {
// React will cache the result and only recalculate when 'data' changes
const sortedData = useMemo(() => {
console.log('Sorting data...');
return [...data].sort((a, b) => a - b);
}, [data]); // Dependency array - recalculate only when data changes
return <div>{sortedData.join(', ')}</div>;
}
π‘ Key Insight
Memoization trades memory (storing cached results) for speed (avoiding recalculation). This is only beneficial when:
- The calculation is expensive (takes noticeable time)
- The same inputs appear multiple times
- The memory cost is acceptable
π― The useMemo Hook
useMemo lets you cache the result of a calculation between re-renders. It only recalculates when dependencies change.
Basic Syntax
import { useMemo } from 'react';
function MyComponent() {
const memoizedValue = useMemo(() => {
// Expensive calculation here
return calculateExpensiveValue();
}, [dependency1, dependency2]); // Dependencies array
return <div>{memoizedValue}</div>;
}
π useMemo Signature
const memoizedValue = useMemo(
() => computeExpensiveValue(a, b),
[a, b]
);
// Returns the memoized value
// Recalculates only when dependencies [a, b] change
How useMemo Works
- First render: React runs your calculation function and stores the result
- Subsequent renders: React checks if dependencies changed
- If unchanged β Returns cached result (fast!)
- If changed β Re-runs calculation and caches new result
Example 1: Filtering a Large List
interface User {
id: number;
name: string;
age: number;
active: boolean;
}
interface UserListProps {
users: User[];
searchTerm: string;
}
function UserList({ users, searchTerm }: UserListProps) {
// Without useMemo - filters on EVERY render
// const filteredUsers = users.filter(user =>
// user.name.toLowerCase().includes(searchTerm.toLowerCase())
// );
// β
With useMemo - only filters when users or searchTerm change
const filteredUsers = useMemo(() => {
console.log('Filtering users...');
return users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [users, searchTerm]);
return (
<div>
<p>Found {filteredUsers.length} users</p>
{filteredUsers.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
// Parent component re-renders due to other state?
// filteredUsers calculation is skipped! (unless users/searchTerm changed)
Example 2: Expensive Calculation
interface DataAnalyticsProps {
data: number[];
}
function DataAnalytics({ data }: DataAnalyticsProps) {
// Expensive statistical calculations
const statistics = useMemo(() => {
console.log('Calculating statistics...');
const sorted = [...data].sort((a, b) => a - b);
const sum = data.reduce((acc, val) => acc + val, 0);
const mean = sum / data.length;
const median = sorted[Math.floor(sorted.length / 2)];
const variance = data.reduce((acc, val) =>
acc + Math.pow(val - mean, 2), 0) / data.length;
const stdDev = Math.sqrt(variance);
return { mean, median, stdDev, min: sorted[0], max: sorted[sorted.length - 1] };
}, [data]); // Only recalculate when data array changes
return (
<div>
<h3>Statistics</h3>
<p>Mean: {statistics.mean.toFixed(2)}</p>
<p>Median: {statistics.median}</p>
<p>Std Dev: {statistics.stdDev.toFixed(2)}</p>
<p>Range: {statistics.min} - {statistics.max}</p>
</div>
);
}
Example 3: Derived State
interface Product {
id: number;
name: string;
price: number;
category: string;
inStock: boolean;
}
interface ProductDashboardProps {
products: Product[];
}
function ProductDashboard({ products }: ProductDashboardProps) {
// Multiple derived values, all memoized
const inStockProducts = useMemo(() =>
products.filter(p => p.inStock),
[products]
);
const totalValue = useMemo(() =>
products.reduce((sum, p) => sum + p.price, 0),
[products]
);
const categoryCounts = useMemo(() => {
const counts: Record<string, number> = {};
products.forEach(p => {
counts[p.category] = (counts[p.category] || 0) + 1;
});
return counts;
}, [products]);
return (
<div>
<h2>Dashboard</h2>
<p>Total Products: {products.length}</p>
<p>In Stock: {inStockProducts.length}</p>
<p>Total Value: ${totalValue.toFixed(2)}</p>
<h3>By Category</h3>
<ul>
{Object.entries(categoryCounts).map(([cat, count]) => (
<li key={cat}>{cat}: {count}</li>
))}
</ul>
</div>
);
}
β When to Use useMemo
- Filtering or sorting large arrays (1000+ items)
- Complex mathematical calculations
- Processing large datasets
- Generating derived data structures
- Expensive object transformations
- When you can measure a performance improvement
β οΈ When NOT to Use useMemo
- Simple calculations (adding two numbers)
- Creating basic objects or arrays
- Premature optimization (measure first!)
- Every single value in your component
- When the calculation is cheaper than memoization overhead
π The useCallback Hook
useCallback is similar to useMemo, but instead of memoizing a value, it memoizes a function itself. This prevents recreating function references on every render.
Basic Syntax
import { useCallback } from 'react';
function MyComponent() {
const memoizedCallback = useCallback(() => {
// Function logic here
doSomething(dependency1, dependency2);
}, [dependency1, dependency2]); // Dependencies array
return <button onClick={memoizedCallback}>Click me</button>;
}
π useCallback Signature
const memoizedFunction = useCallback(
(args) => {
// Function body
},
[dependencies]
);
// Returns the memoized function reference
// Recreates only when dependencies change
Why Function References Matter
In JavaScript, every function is a unique object. Creating a function twice creates two different references, even if the code is identical:
// These are NOT equal!
const func1 = () => console.log('hello');
const func2 = () => console.log('hello');
console.log(func1 === func2); // false - different references!
// This causes problems in React
function Parent() {
// This creates a NEW function on EVERY render
const handleClick = () => {
console.log('clicked');
};
// Child receives a "new" function every render
// Even though the function does the exact same thing!
return <Child onClick={handleClick} />;
}
The Problem: Functions Recreated Every Render
// β Problem: Child re-renders unnecessarily
const Child = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// New function reference on every render!
const handleClick = () => {
console.log('Button clicked');
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* Child re-renders even though handleClick logic didn't change */}
<Child onClick={handleClick} />
</div>
);
}
// Every time count changes, Child re-renders
// Because handleClick is a new reference each time!
The Solution: useCallback
// β
Solution: Stable function reference
const Child = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Same function reference across renders!
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // No dependencies = never recreated
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* Child doesn't re-render when count changes! */}
<Child onClick={handleClick} />
</div>
);
}
// Now Child only renders when it actually needs to!
Example 1: Passing Callbacks to Memoized Children
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
// Memoized child component
const TodoItem = React.memo(({ todo, onToggle, onDelete }: TodoItemProps) => {
console.log(`Rendering todo: ${todo.text}`);
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
});
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Learn TypeScript', completed: true }
]);
// β
Memoized callbacks - stable references
const handleToggle = useCallback((id: number) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []); // No dependencies needed - uses functional update
const handleDelete = useCallback((id: number) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</div>
);
}
// Only the affected TodoItem re-renders, not all of them!
Example 2: useCallback with Dependencies
interface SearchProps {
onSearch: (query: string) => void;
}
function SearchComponent() {
const [query, setQuery] = useState('');
const [filter, setFilter] = useState('all');
// Callback depends on filter
const handleSearch = useCallback((searchTerm: string) => {
console.log(`Searching for "${searchTerm}" with filter: ${filter}`);
// API call with query and filter
fetchResults(searchTerm, filter);
}, [filter]); // Recreate when filter changes
return (
<div>
<select value={filter} onChange={e => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
<SearchInput onSearch={handleSearch} />
</div>
);
}
Example 3: Event Handlers with State
interface FormProps {
onSubmit: (data: FormData) => void;
}
function ComplexForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [status, setStatus] = useState('idle');
// Callback uses current formData
const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
setStatus('submitting');
// Use current formData values
submitForm(formData)
.then(() => setStatus('success'))
.catch(() => setStatus('error'));
}, [formData]); // Recreate when formData changes
// Individual field handlers
const handleNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, name: e.target.value }));
}, []);
const handleEmailChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, email: e.target.value }));
}, []);
return (
<form onSubmit={handleSubmit}>
<input value={formData.name} onChange={handleNameChange} />
<input value={formData.email} onChange={handleEmailChange} />
<button type="submit" disabled={status === 'submitting'}>
Submit
</button>
</form>
);
}
β When to Use useCallback
- Passing callbacks to memoized child components (React.memo)
- Functions used as dependencies in useEffect or other hooks
- Event handlers passed to expensive-to-render children
- Callbacks passed to custom hooks
- Functions that create subscriptions or WebSocket connections
- When profiling shows unnecessary child re-renders
β οΈ When NOT to Use useCallback
- Every function in your component (overkill!)
- Functions not passed as props or dependencies
- Simple event handlers on native DOM elements
- Functions in components that rarely re-render
- When the child component isn't memoized anyway
βοΈ useMemo vs useCallback
These two hooks are closely related but serve different purposes. Let's understand the key differences and when to use each.
The Fundamental Difference
| Aspect | useMemo | useCallback |
|---|---|---|
| What it memoizes | The result of a function | The function itself |
| Returns | Any value (number, object, array, etc.) | A function reference |
| Use case | Expensive calculations | Stable function references |
| Example | Sorting/filtering large arrays | Event handlers for memoized children |
Side-by-Side Comparison
function ComparisonExample({ data }: { data: number[] }) {
// useMemo - memoizes the RESULT (sorted array)
const sortedData = useMemo(() => {
return [...data].sort((a, b) => a - b); // Returns sorted array
}, [data]);
// sortedData is the array: [1, 2, 3, 4, 5]
// useCallback - memoizes the FUNCTION
const handleSort = useCallback(() => {
console.log('Sorting data');
return [...data].sort((a, b) => a - b); // Returns function
}, [data]);
// handleSort is the function: () => {...}
return (
<div>
{/* useMemo: Use the value directly */}
<p>Sorted: {sortedData.join(', ')}</p>
{/* useCallback: Call the function when needed */}
<button onClick={handleSort}>Sort</button>
</div>
);
}
Interchangeability
Technically, useCallback is just syntactic sugar for useMemo that returns a function:
// These are equivalent:
// Using useCallback
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// Using useMemo to return a function
const handleClick = useMemo(() => {
return () => {
console.log('clicked');
};
}, []);
// But useCallback is clearer and more readable!
Real-World Example: Both Together
interface Product {
id: number;
name: string;
price: number;
category: string;
}
interface ProductListProps {
products: Product[];
}
function ProductList({ products }: ProductListProps) {
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
// useMemo: Memoize the filtered and sorted products (result)
const processedProducts = useMemo(() => {
console.log('Processing products...');
// Filter
let filtered = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// Sort
filtered.sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
}
return a.price - b.price;
});
return filtered;
}, [products, searchTerm, sortBy]);
// processedProducts is the array of processed products
// useCallback: Memoize the search handler (function)
const handleSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(e.target.value);
}, []);
// handleSearch is the function itself
// useCallback: Memoize the sort handler (function)
const handleSortChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
setSortBy(e.target.value as 'name' | 'price');
}, []);
return (
<div>
<input
type="search"
value={searchTerm}
onChange={handleSearch}
placeholder="Search products..."
/>
<select value={sortBy} onChange={handleSortChange}>
<option value="name">Sort by Name</option>
<option value="price">Sort by Price</option>
</select>
<p>Showing {processedProducts.length} products</p>
{processedProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
Decision Tree
π‘ Quick Reference
- useMemo: "Remember this result" β Returns the calculated value
- useCallback: "Remember this function" β Returns the function itself
- Both prevent recreation when dependencies don't change
- Both have the same dependency array mechanics
- Both can hurt performance if overused
π― When to Use (and When NOT to Use)
The most important skill with useMemo and useCallback is knowing when they help and when they hurt. Let's build that intuition.
The Golden Rule
π― Golden Rule of Memoization
"Only optimize when you can measure a problem."
Don't memoize everything "just in case". Memoization has costs too!
The Cost of Memoization
useMemo and useCallback aren't free! They have costs:
- Memory: Storing cached values takes memory
- Comparison: React must compare dependencies on every render
- Complexity: More code to read and maintain
- Bugs: Wrong dependencies can cause subtle bugs
// β Over-optimization - the cost outweighs the benefit!
function SimpleCounter() {
const [count, setCount] = useState(0);
// Bad: Memoizing a simple addition
const doubleCount = useMemo(() => count * 2, [count]);
// Bad: Memoizing a simple string
const message = useMemo(() => `Count is ${count}`, [count]);
// Bad: Memoizing a simple function
const increment = useCallback(() => setCount(c => c + 1), []);
return (
<div>
<p>{message}</p>
<p>Double: {doubleCount}</p>
<button onClick={increment}>+</button>
</div>
);
}
// β
Better: Keep it simple
function SimpleCounter() {
const [count, setCount] = useState(0);
// These are so cheap, memoization adds overhead instead of removing it
const doubleCount = count * 2;
const message = `Count is ${count}`;
return (
<div>
<p>{message}</p>
<p>Double: {doubleCount}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
Good Use Cases β
1. Filtering/Sorting Large Lists
// β
Good: Expensive operation on large dataset
function UserTable({ users }: { users: User[] }) { // 10,000+ users
const [sortKey, setSortKey] = useState('name');
const sortedUsers = useMemo(() => {
return [...users].sort((a, b) =>
a[sortKey].localeCompare(b[sortKey])
);
}, [users, sortKey]);
// Without useMemo, sorting 10,000 items on every render is expensive!
}
2. Complex Calculations
// β
Good: Computationally expensive
function DataVisualization({ data }: { data: number[] }) {
const statistics = useMemo(() => {
// Multiple passes through large dataset
const sorted = [...data].sort((a, b) => a - b);
const mean = data.reduce((a, b) => a + b) / data.length;
const median = sorted[Math.floor(data.length / 2)];
const mode = calculateMode(data); // Custom expensive function
const stdDev = calculateStandardDeviation(data, mean);
return { mean, median, mode, stdDev };
}, [data]);
// These calculations are worth caching
}
3. Preventing Child Re-renders
// β
Good: Child is memoized and expensive to render
const ExpensiveList = React.memo(({ items, onItemClick }: Props) => {
console.log('Rendering 1000+ items...');
return (
<div>
{items.map(item => (
<ExpensiveItem key={item.id} item={item} onClick={onItemClick} />
))}
</div>
);
});
function Parent() {
const [unrelatedState, setUnrelatedState] = useState(0);
const items = [...]; // Large array
// Without useCallback, ExpensiveList re-renders when unrelatedState changes
const handleItemClick = useCallback((id: number) => {
console.log('Clicked:', id);
}, []);
return <ExpensiveList items={items} onItemClick={handleItemClick} />;
}
Bad Use Cases β
1. Simple Calculations
// β Bad: Over-optimization
function Profile({ user }: { user: User }) {
// These are instant - don't memoize!
const fullName = useMemo(() =>
`${user.firstName} ${user.lastName}`, [user]); // Overkill!
const age = useMemo(() =>
new Date().getFullYear() - user.birthYear, [user]); // Overkill!
// Just do them directly:
// const fullName = `${user.firstName} ${user.lastName}`;
// const age = new Date().getFullYear() - user.birthYear;
}
2. Callbacks Not Passed to Memoized Children
// β Bad: Unnecessary complexity
function Form() {
const [name, setName] = useState('');
// Pointless - just attached to a regular input
const handleChange = useCallback((e) => {
setName(e.target.value);
}, []);
// Regular input doesn't benefit from stable function reference
return <input onChange={handleChange} />;
// Better: <input onChange={e => setName(e.target.value)} />
}
3. Dependencies That Always Change
// β Bad: Defeats the purpose
function Component({ config }: { config: Config }) {
// config is a new object every render, so this never caches!
const processed = useMemo(() => {
return processConfig(config);
}, [config]); // config is always "new"
// useMemo does nothing here - it recalculates every time anyway!
}
The Checklist
β Use useMemo when:
- β The calculation is measurably expensive (100ms+)
- β It involves large datasets (1000+ items)
- β Profiling shows it as a bottleneck
- β Dependencies change rarely
- β The value is passed to memoized children
β Use useCallback when:
- β Function is passed to React.memo'd children
- β Function is a dependency in useEffect
- β Child component re-renders are expensive
- β Function is passed to custom hooks
- β Profiling shows unnecessary child re-renders
β οΈ Don't Use When:
- β The operation is instant (< 1ms)
- β You're just guessing it might help
- β The component rarely re-renders anyway
- β Dependencies change on every render
- β Children aren't memoized (for useCallback)
- β You haven't measured the problem
π Typing with TypeScript
TypeScript makes useMemo and useCallback even more powerful by catching type errors at compile time. Let's explore how to type these hooks properly.
Basic TypeScript with useMemo
import { useMemo } from 'react';
function Component() {
// TypeScript infers the type automatically
const count = useMemo(() => {
return 42; // TypeScript knows this is number
}, []);
// count: number
// You can also explicitly type it
const message = useMemo<string>(() => {
return 'Hello World';
}, []);
// message: string
// Arrays and objects are inferred too
const users = useMemo(() => {
return [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
}, []);
// users: { id: number; name: string; }[]
}
Typing Complex Return Values
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
interface UserStats {
total: number;
admins: number;
activeUsers: User[];
}
function UserDashboard({ users }: { users: User[] }) {
// Explicitly type the return value for clarity
const stats = useMemo<UserStats>(() => {
return {
total: users.length,
admins: users.filter(u => u.role === 'admin').length,
activeUsers: users.filter(u => u.role === 'user')
};
}, [users]);
// TypeScript knows stats has these properties
return (
<div>
<p>Total: {stats.total}</p>
<p>Admins: {stats.admins}</p>
<p>Active: {stats.activeUsers.length}</p>
</div>
);
}
Basic TypeScript with useCallback
import { useCallback } from 'react';
function Component() {
// TypeScript infers parameter and return types
const handleClick = useCallback((id: number) => {
console.log('Clicked:', id);
}, []);
// handleClick: (id: number) => void
// Explicitly typing event handlers
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
}, []);
// handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
// Functions that return values
const calculateTotal = useCallback((items: number[]): number => {
return items.reduce((sum, item) => sum + item, 0);
}, []);
// calculateTotal: (items: number[]) => number
}
Typing Generic Callbacks
interface Item {
id: number;
name: string;
}
interface ListProps<T> {
items: T[];
onItemClick: (item: T) => void;
onItemDelete: (id: number) => void;
}
function GenericList<T extends { id: number }>({
items,
onItemClick,
onItemDelete
}: ListProps<T>) {
// Callback that works with generic type
const handleClick = useCallback((item: T) => {
onItemClick(item);
}, [onItemClick]);
// Type-safe delete handler
const handleDelete = useCallback((id: number) => {
onItemDelete(id);
}, [onItemDelete]);
return (
<div>
{items.map(item => (
<div key={item.id}>
<button onClick={() => handleClick(item)}>View</button>
<button onClick={() => handleDelete(item.id)}>Delete</button>
</div>
))}
</div>
);
}
Typing Async Callbacks
interface FetchResult<T> {
data: T | null;
error: Error | null;
loading: boolean;
}
function DataFetcher() {
const [result, setResult] = useState<FetchResult<User>>({
data: null,
error: null,
loading: false
});
// Async callback with proper typing
const fetchUser = useCallback(async (id: number): Promise<void> => {
setResult({ data: null, error: null, loading: true });
try {
const response = await fetch(`/api/users/${id}`);
const data: User = await response.json();
setResult({ data, error: null, loading: false });
} catch (error) {
setResult({
data: null,
error: error as Error,
loading: false
});
}
}, []);
// fetchUser: (id: number) => Promise<void>
return (
<button onClick={() => fetchUser(1)}>
Load User
</button>
);
}
Typing with Utility Types
interface Product {
id: number;
name: string;
price: number;
stock: number;
}
function ProductManager() {
const [products, setProducts] = useState<Product[]>([]);
// Using Partial for updates
const updateProduct = useCallback((
id: number,
updates: Partial<Product>
): void => {
setProducts(prev => prev.map(p =>
p.id === id ? { ...p, ...updates } : p
));
}, []);
// Using Pick to select specific fields
const updatePrice = useCallback((
id: number,
priceUpdate: Pick<Product, 'price'>
): void => {
setProducts(prev => prev.map(p =>
p.id === id ? { ...p, price: priceUpdate.price } : p
));
}, []);
// Using Omit to exclude fields
const createProduct = useCallback((
product: Omit<Product, 'id'>
): void => {
const newProduct: Product = {
...product,
id: Date.now()
};
setProducts(prev => [...prev, newProduct]);
}, []);
return <div>{/* UI */}</div>;
}
Typing Hook Dependencies
interface SearchOptions {
query: string;
filters: string[];
sortBy: string;
}
function SearchComponent({ options }: { options: SearchOptions }) {
// Dependencies are type-checked
const results = useMemo(() => {
return performSearch(options);
}, [options]); // TypeScript ensures 'options' matches the type
// Wrong dependencies cause type errors
const handleSearch = useCallback(() => {
console.log(options.query);
}, []); // β οΈ TypeScript warns: options should be in dependencies!
// Correct dependencies
const handleSearchCorrect = useCallback(() => {
console.log(options.query);
}, [options]); // β
TypeScript is happy
}
β TypeScript Best Practices
- Let TypeScript infer types when possible - it's usually right
- Use explicit types for complex return values or when clarity helps
- Leverage utility types (Partial, Pick, Omit) for flexibility
- Type event handlers properly (React.ChangeEvent, React.MouseEvent, etc.)
- Use generics for reusable components and hooks
- Let TypeScript catch missing or wrong dependencies
π Real-World Examples
Let's see how useMemo and useCallback work in real production scenarios.
Example 1: Data Table with Sorting and Filtering
interface Employee {
id: number;
name: string;
department: string;
salary: number;
hireDate: Date;
}
interface DataTableProps {
employees: Employee[];
}
function EmployeeTable({ employees }: DataTableProps) {
const [searchTerm, setSearchTerm] = useState('');
const [sortField, setSortField] = useState<keyof Employee>('name');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [departmentFilter, setDepartmentFilter] = useState('all');
// Memoize filtered and sorted data
const processedEmployees = useMemo(() => {
console.log('Processing employees...');
// Filter by search term
let filtered = employees.filter(emp =>
emp.name.toLowerCase().includes(searchTerm.toLowerCase())
);
// Filter by department
if (departmentFilter !== 'all') {
filtered = filtered.filter(emp => emp.department === departmentFilter);
}
// Sort
filtered.sort((a, b) => {
const aVal = a[sortField];
const bVal = b[sortField];
if (typeof aVal === 'string' && typeof bVal === 'string') {
return sortDirection === 'asc'
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal);
}
if (typeof aVal === 'number' && typeof bVal === 'number') {
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
}
return 0;
});
return filtered;
}, [employees, searchTerm, sortField, sortDirection, departmentFilter]);
// Memoize sort handler
const handleSort = useCallback((field: keyof Employee) => {
if (field === sortField) {
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
}, [sortField]);
// Get unique departments for filter
const departments = useMemo(() => {
const depts = new Set(employees.map(e => e.department));
return Array.from(depts);
}, [employees]);
return (
<div>
<input
type="search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search employees..."
/>
<select
value={departmentFilter}
onChange={(e) => setDepartmentFilter(e.target.value)}
>
<option value="all">All Departments</option>
{departments.map(dept => (
<option key={dept} value={dept}>{dept}</option>
))}
</select>
<table>
<thead>
<tr>
<th onClick={() => handleSort('name')}>Name</th>
<th onClick={() => handleSort('department')}>Department</th>
<th onClick={() => handleSort('salary')}>Salary</th>
</tr>
</thead>
<tbody>
{processedEmployees.map(emp => (
<tr key={emp.id}>
<td>{emp.name}</td>
<td>{emp.department}</td>
<td>${emp.salary.toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
<p>Showing {processedEmployees.length} of {employees.length} employees</p>
</div>
);
}
Example 2: Shopping Cart with Calculations
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
taxRate: number;
}
interface CartSummary {
subtotal: number;
tax: number;
total: number;
itemCount: number;
}
function ShoppingCart() {
const [items, setItems] = useState<CartItem[]>([]);
const [discountCode, setDiscountCode] = useState('');
// Calculate cart summary
const summary = useMemo<CartSummary>(() => {
const subtotal = items.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
const tax = items.reduce(
(sum, item) => sum + (item.price * item.quantity * item.taxRate),
0
);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
let total = subtotal + tax;
// Apply discount
if (discountCode === 'SAVE10') {
total *= 0.9;
}
return { subtotal, tax, total, itemCount };
}, [items, discountCode]);
// Memoized update handlers
const updateQuantity = useCallback((id: number, quantity: number) => {
setItems(prev => prev.map(item =>
item.id === id ? { ...item, quantity: Math.max(0, quantity) } : item
));
}, []);
const removeItem = useCallback((id: number) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
const clearCart = useCallback(() => {
setItems([]);
setDiscountCode('');
}, []);
return (
<div>
<h2>Shopping Cart ({summary.itemCount} items)</h2>
{items.map(item => (
<CartItemRow
key={item.id}
item={item}
onUpdateQuantity={updateQuantity}
onRemove={removeItem}
/>
))}
<div className="cart-summary">
<p>Subtotal: ${summary.subtotal.toFixed(2)}</p>
<p>Tax: ${summary.tax.toFixed(2)}</p>
<p>Total: ${summary.total.toFixed(2)}</p>
</div>
<input
type="text"
value={discountCode}
onChange={(e) => setDiscountCode(e.target.value.toUpperCase())}
placeholder="Discount code"
/>
<button onClick={clearCart}>Clear Cart</button>
</div>
);
}
// Memoized child component
const CartItemRow = React.memo(({
item,
onUpdateQuantity,
onRemove
}: {
item: CartItem;
onUpdateQuantity: (id: number, quantity: number) => void;
onRemove: (id: number) => void;
}) => {
console.log(`Rendering cart item: ${item.name}`);
return (
<div className="cart-item">
<span>{item.name}</span>
<span>${item.price.toFixed(2)}</span>
<input
type="number"
value={item.quantity}
onChange={(e) => onUpdateQuantity(item.id, Number(e.target.value))}
min="0"
/>
<span>${(item.price * item.quantity).toFixed(2)}</span>
<button onClick={() => onRemove(item.id)}>Remove</button>
</div>
);
});
Example 3: Search with Debouncing
interface SearchResult {
id: number;
title: string;
description: string;
}
function SearchWithDebounce() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
// Debounce the search query
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(query);
}, 500);
return () => clearTimeout(timer);
}, [query]);
// Memoized search function
const performSearch = useCallback(async (searchQuery: string) => {
if (!searchQuery.trim()) {
setResults([]);
return;
}
setIsSearching(true);
try {
const response = await fetch(`/api/search?q=${searchQuery}`);
const data: SearchResult[] = await response.json();
setResults(data);
} catch (error) {
console.error('Search failed:', error);
setResults([]);
} finally {
setIsSearching(false);
}
}, []);
// Execute search when debounced query changes
useEffect(() => {
performSearch(debouncedQuery);
}, [debouncedQuery, performSearch]);
// Memoized highlight function
const highlightMatch = useCallback((text: string, query: string): string => {
if (!query) return text;
const regex = new RegExp(`(${query})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}, []);
// Memoized filtered results (client-side refinement)
const displayResults = useMemo(() => {
return results.slice(0, 10); // Show top 10
}, [results]);
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{isSearching && <p>Searching...</p>}
<div className="results">
{displayResults.map(result => (
<div key={result.id} className="result">
<h3 dangerouslySetInnerHTML={{
__html: highlightMatch(result.title, query)
}} />
<p>{result.description}</p>
</div>
))}
</div>
{!isSearching && query && displayResults.length === 0 && (
<p>No results found for "{query}"</p>
)}
</div>
);
}
Example 4: Form Validation with Complex Rules
interface FormData {
email: string;
password: string;
confirmPassword: string;
age: number;
terms: boolean;
}
interface ValidationErrors {
[key: string]: string;
}
function RegistrationForm() {
const [formData, setFormData] = useState<FormData>({
email: '',
password: '',
confirmPassword: '',
age: 0,
terms: false
});
// Memoized validation
const errors = useMemo<ValidationErrors>(() => {
const newErrors: ValidationErrors = {};
// Email validation
if (formData.email && !formData.email.includes('@')) {
newErrors.email = 'Invalid email address';
}
// Password strength
if (formData.password && formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
if (formData.password && !/[A-Z]/.test(formData.password)) {
newErrors.password = 'Password must contain uppercase letter';
}
// Password match
if (formData.confirmPassword &&
formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
// Age validation
if (formData.age && formData.age < 18) {
newErrors.age = 'Must be 18 or older';
}
// Terms
if (!formData.terms) {
newErrors.terms = 'You must accept the terms';
}
return newErrors;
}, [formData]);
// Check if form is valid
const isValid = useMemo(() => {
return Object.keys(errors).length === 0 &&
formData.email &&
formData.password &&
formData.confirmPassword &&
formData.age > 0;
}, [errors, formData]);
// Memoized field update handler
const updateField = useCallback((field: keyof FormData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
}, []);
// Memoized submit handler
const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
if (!isValid) {
alert('Please fix errors before submitting');
return;
}
console.log('Submitting:', formData);
// API call here
}, [isValid, formData]);
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
value={formData.email}
onChange={(e) => updateField('email', e.target.value)}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input
type="password"
value={formData.password}
onChange={(e) => updateField('password', e.target.value)}
placeholder="Password"
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<div>
<input
type="password"
value={formData.confirmPassword}
onChange={(e) => updateField('confirmPassword', e.target.value)}
placeholder="Confirm Password"
/>
{errors.confirmPassword &&
<span className="error">{errors.confirmPassword}</span>}
</div>
<div>
<input
type="number"
value={formData.age || ''}
onChange={(e) => updateField('age', Number(e.target.value))}
placeholder="Age"
/>
{errors.age && <span className="error">{errors.age}</span>}
</div>
<div>
<label>
<input
type="checkbox"
checked={formData.terms}
onChange={(e) => updateField('terms', e.target.checked)}
/>
I accept the terms and conditions
</label>
{errors.terms && <span className="error">{errors.terms}</span>}
</div>
<button type="submit" disabled={!isValid}>
Register
</button>
</form>
);
}
π‘ Real-World Patterns
- Data tables: useMemo for filtering/sorting large datasets
- Shopping carts: useMemo for totals, useCallback for item operations
- Search: useCallback for debounced searches, useMemo for results processing
- Forms: useMemo for validation, useCallback for field updates
- Dashboards: useMemo for aggregated statistics from large datasets
π Performance Profiling
Before you optimize, you need to measure! Let's learn how to profile your React app and identify real performance bottlenecks.
React DevTools Profiler
React DevTools includes a Profiler that shows you exactly what's rendering and how long it takes.
β Setting Up React DevTools Profiler
- Install React DevTools browser extension
- Open your React app in development mode
- Open browser DevTools β React Profiler tab
- Click the record button (βΊοΈ)
- Interact with your app
- Click stop (βΉοΈ) to see results
Reading Profiler Results
// Add console.log to see re-renders
function MyComponent({ data }: { data: number[] }) {
console.log('MyComponent rendered');
const sortedData = useMemo(() => {
console.log('Sorting data...');
return [...data].sort((a, b) => a - b);
}, [data]);
return <div>{sortedData.join(', ')}</div>;
}
// Without useMemo:
// Parent re-renders β "MyComponent rendered" + "Sorting data..."
// With useMemo:
// Parent re-renders β "MyComponent rendered" only (sorting skipped!)
Using the Profiler API
React provides a Profiler component to programmatically measure performance:
import { Profiler, ProfilerOnRenderCallback } from 'react';
function App() {
const onRenderCallback: ProfilerOnRenderCallback = (
id, // Component name
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime, // When render started
commitTime // When render committed
) => {
console.log(`${id} (${phase}): ${actualDuration}ms`);
if (actualDuration > 100) {
console.warn(`Slow render detected in ${id}!`);
}
};
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
);
}
Performance Testing Pattern
function PerformanceTest() {
const [data] = useState(() =>
Array.from({ length: 10000 }, (_, i) => i)
);
const [trigger, setTrigger] = useState(0);
// Test WITHOUT useMemo
console.time('Without useMemo');
const sortedWithout = [...data].sort((a, b) => b - a);
console.timeEnd('Without useMemo');
// Test WITH useMemo
console.time('With useMemo');
const sortedWith = useMemo(() => {
return [...data].sort((a, b) => b - a);
}, [data]);
console.timeEnd('With useMemo');
return (
<div>
<button onClick={() => setTrigger(t => t + 1)}>
Force Re-render ({trigger})
</button>
<p>Check console for timing results</p>
</div>
);
// First render: Both take similar time
// Re-renders: useMemo is instant, without is slow!
}
Measuring Child Re-renders
// Create a render counter
let renderCount = 0;
const ExpensiveChild = React.memo(({
data,
onClick
}: {
data: number[];
onClick: () => void
}) => {
renderCount++;
console.log(`ExpensiveChild render #${renderCount}`);
return (
<div>
<p>Rendered {renderCount} times</p>
<button onClick={onClick}>Click</button>
</div>
);
});
function Parent() {
const [count, setCount] = useState(0);
const data = [1, 2, 3];
// Without useCallback
// const handleClick = () => console.log('clicked');
// With useCallback
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Parent count: {count}
</button>
<ExpensiveChild data={data} onClick={handleClick} />
</div>
);
}
// Without useCallback: Child re-renders every time parent count changes
// With useCallback: Child only renders once!
Browser Performance Tab
π‘ Using Browser Performance Tools
- Open Chrome DevTools β Performance tab
- Click record, interact with your app, then stop
- Look for:
- Long tasks: JavaScript execution over 50ms
- Excessive re-renders: Many React commits
- Layout thrashing: Repeated layout calculations
- Identify expensive functions in the flame graph
- Optimize those specific areas
Before and After Comparison
// Create a performance comparison
function PerformanceComparison() {
const [withMemo, setWithMemo] = useState(true);
const [data] = useState(() =>
Array.from({ length: 50000 }, (_, i) => ({
id: i,
value: Math.random()
}))
);
const startTime = performance.now();
// Version WITH useMemo
const processedWithMemo = useMemo(() => {
return data
.filter(item => item.value > 0.5)
.sort((a, b) => b.value - a.value)
.slice(0, 100);
}, [data]);
// Version WITHOUT useMemo
const processedWithout = data
.filter(item => item.value > 0.5)
.sort((a, b) => b.value - a.value)
.slice(0, 100);
const endTime = performance.now();
const duration = endTime - startTime;
const result = withMemo ? processedWithMemo : processedWithout;
return (
<div>
<button onClick={() => setWithMemo(!withMemo)}>
Toggle: {withMemo ? 'WITH' : 'WITHOUT'} useMemo
</button>
<p>Render time: {duration.toFixed(2)}ms</p>
<p>Items: {result.length}</p>
</div>
);
}
β οΈ Profiling Best Practices
- Always profile in production mode for accurate results
- Profile with realistic data volumes (not just 10 items!)
- Test on lower-end devices, not just your powerful dev machine
- Compare before and after optimization to confirm improvement
- Focus on the 80/20 rule - optimize the biggest bottlenecks first
- Don't optimize based on assumptions - measure first!
ποΈ Hands-on Practice
Time to apply what you've learned! Complete these exercises to master useMemo and useCallback.
ποΈ Exercise 1: Optimize a Product Filter
Goal: Use useMemo to optimize filtering and sorting a product list.
Starting Code:
View Starting Code
interface Product {
id: number;
name: string;
price: number;
category: string;
rating: number;
}
function ProductList({ products }: { products: Product[] }) {
const [searchTerm, setSearchTerm] = useState('');
const [minPrice, setMinPrice] = useState(0);
const [sortBy, setSortBy] = useState<'price' | 'rating'>('price');
// π΄ OPTIMIZE THIS - it runs every render!
const filteredProducts = products
.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()))
.filter(p => p.price >= minPrice)
.sort((a, b) => b[sortBy] - a[sortBy]);
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<input
type="number"
value={minPrice}
onChange={e => setMinPrice(Number(e.target.value))}
placeholder="Min price"
/>
<select value={sortBy} onChange={e => setSortBy(e.target.value as any)}>
<option value="price">Price</option>
<option value="rating">Rating</option>
</select>
{filteredProducts.map(p => (
<div key={p.id}>{p.name} - ${p.price}</div>
))}
</div>
);
}
π‘ Hint
Wrap the filtering and sorting logic in useMemo with proper dependencies: [products, searchTerm, minPrice, sortBy]
β Solution
function ProductList({ products }: { products: Product[] }) {
const [searchTerm, setSearchTerm] = useState('');
const [minPrice, setMinPrice] = useState(0);
const [sortBy, setSortBy] = useState<'price' | 'rating'>('price');
// β
Optimized with useMemo
const filteredProducts = useMemo(() => {
console.log('Filtering and sorting products...');
return products
.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()))
.filter(p => p.price >= minPrice)
.sort((a, b) => b[sortBy] - a[sortBy]);
}, [products, searchTerm, minPrice, sortBy]);
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<input
type="number"
value={minPrice}
onChange={e => setMinPrice(Number(e.target.value))}
placeholder="Min price"
/>
<select value={sortBy} onChange={e => setSortBy(e.target.value as any)}>
<option value="price">Price</option>
<option value="rating">Rating</option>
</select>
{filteredProducts.map(p => (
<div key={p.id}>{p.name} - ${p.price}</div>
))}
</div>
);
}
ποΈ Exercise 2: Fix Unnecessary Child Re-renders
Goal: Use useCallback to prevent unnecessary re-renders of memoized children.
Starting Code:
View Starting Code
const TaskItem = React.memo(({
task,
onToggle,
onDelete
}: {
task: { id: number; text: string; done: boolean };
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}) => {
console.log(`Rendering task: ${task.text}`);
return (
<div>
<input
type="checkbox"
checked={task.done}
onChange={() => onToggle(task.id)}
/>
<span>{task.text}</span>
<button onClick={() => onDelete(task.id)}>Delete</button>
</div>
);
});
function TaskList() {
const [tasks, setTasks] = useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Learn TypeScript', done: true }
]);
const [newTask, setNewTask] = useState('');
// π΄ PROBLEM: These recreate on every render
const handleToggle = (id: number) => {
setTasks(prev => prev.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
};
const handleDelete = (id: number) => {
setTasks(prev => prev.filter(t => t.id !== id));
};
return (
<div>
<input
value={newTask}
onChange={e => setNewTask(e.target.value)}
/>
{tasks.map(task => (
<TaskItem
key={task.id}
task={task}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</div>
);
}
π‘ Hint
Wrap handleToggle and handleDelete with useCallback. Since they use functional updates (prev =>), they don't need any dependencies!
β Solution
function TaskList() {
const [tasks, setTasks] = useState([
{ id: 1, text: 'Learn React', done: false },
{ id: 2, text: 'Learn TypeScript', done: true }
]);
const [newTask, setNewTask] = useState('');
// β
Optimized with useCallback
const handleToggle = useCallback((id: number) => {
setTasks(prev => prev.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
}, []); // No dependencies needed with functional update!
const handleDelete = useCallback((id: number) => {
setTasks(prev => prev.filter(t => t.id !== id));
}, []);
return (
<div>
<input
value={newTask}
onChange={e => setNewTask(e.target.value)}
/>
{tasks.map(task => (
<TaskItem
key={task.id}
task={task}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</div>
);
}
// Now typing in the input doesn't re-render all TaskItems!
ποΈ Exercise 3: Build a Data Dashboard
Goal: Create a dashboard that efficiently calculates statistics from a large dataset.
Requirements:
- Display total count, average, min, max, and sum
- Allow filtering by value range
- Use useMemo for all calculations
- Handle datasets with 10,000+ numbers
β Solution
function DataDashboard() {
const [data] = useState(() =>
Array.from({ length: 10000 }, () => Math.random() * 1000)
);
const [minFilter, setMinFilter] = useState(0);
const [maxFilter, setMaxFilter] = useState(1000);
// Filter data
const filteredData = useMemo(() => {
console.log('Filtering data...');
return data.filter(n => n >= minFilter && n <= maxFilter);
}, [data, minFilter, maxFilter]);
// Calculate statistics
const stats = useMemo(() => {
console.log('Calculating statistics...');
if (filteredData.length === 0) {
return { count: 0, avg: 0, min: 0, max: 0, sum: 0 };
}
const sum = filteredData.reduce((acc, val) => acc + val, 0);
const avg = sum / filteredData.length;
const min = Math.min(...filteredData);
const max = Math.max(...filteredData);
return { count: filteredData.length, avg, min, max, sum };
}, [filteredData]);
return (
<div>
<h2>Data Dashboard</h2>
<div>
<label>Min: <input
type="number"
value={minFilter}
onChange={e => setMinFilter(Number(e.target.value))}
/></label>
<label>Max: <input
type="number"
value={maxFilter}
onChange={e => setMaxFilter(Number(e.target.value))}
/></label>
</div>
<div className="stats">
<p>Count: {stats.count}</p>
<p>Average: {stats.avg.toFixed(2)}</p>
<p>Min: {stats.min.toFixed(2)}</p>
<p>Max: {stats.max.toFixed(2)}</p>
<p>Sum: {stats.sum.toFixed(2)}</p>
</div>
</div>
);
}
ποΈ Challenge: Build an Optimized Autocomplete
Goal: Create a performant autocomplete component that searches through 1000+ items.
Requirements:
- Search through array of 1000+ strings
- Show top 10 matches
- Highlight matching text
- Optimize with useMemo and useCallback
- Handle keyboard navigation (up/down arrows)
β Solution
function Autocomplete({ items }: { items: string[] }) {
const [query, setQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const [isOpen, setIsOpen] = useState(false);
// Search and filter
const matches = useMemo(() => {
if (!query) return [];
console.log('Searching...');
return items
.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
)
.slice(0, 10); // Top 10 results
}, [items, query]);
// Highlight helper
const highlightMatch = useCallback((text: string, query: string) => {
if (!query) return text;
const parts = text.split(new RegExp(`(${query})`, 'gi'));
return parts.map((part, i) =>
part.toLowerCase() === query.toLowerCase()
? `<mark>${part}</mark>`
: part
).join('');
}, []);
// Keyboard navigation
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (!isOpen) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex(i => Math.min(i + 1, matches.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex(i => Math.max(i - 1, 0));
} else if (e.key === 'Enter' && matches[selectedIndex]) {
setQuery(matches[selectedIndex]);
setIsOpen(false);
}
}, [isOpen, matches, selectedIndex]);
return (
<div>
<input
value={query}
onChange={e => {
setQuery(e.target.value);
setIsOpen(true);
setSelectedIndex(0);
}}
onKeyDown={handleKeyDown}
placeholder="Search..."
/>
{isOpen && matches.length > 0 && (
<ul>
{matches.map((match, i) => (
<li
key={i}
className={i === selectedIndex ? 'selected' : ''}
onClick={() => {
setQuery(match);
setIsOpen(false);
}}
dangerouslySetInnerHTML={{
__html: highlightMatch(match, query)
}}
/>
))}
</ul>
)}
</div>
);
}
β Best Practices
Follow these guidelines to use useMemo and useCallback effectively.
β Do's
- Measure first: Profile your app before optimizing - don't guess
- Use for expensive operations: Large list operations, complex calculations
- Optimize child re-renders: Use useCallback with React.memo'd children
- List all dependencies: React DevTools will warn you about missing ones
- Use functional updates: Avoid dependencies when possible with setState(prev => ...)
- Document why you're memoizing: Add comments explaining the optimization
- Memoize expensive custom hooks: If a hook does heavy calculations
- Use with Context providers: Memoize context values to prevent cascade re-renders
- Combine with React.memo: Maximum benefit when child is memoized too
- Test the optimization: Verify it actually improves performance
β Don'ts
- Don't memoize everything: It adds overhead and complexity
- Don't use for simple calculations: Adding two numbers doesn't need useMemo
- Don't memoize primitives: Numbers, strings, booleans are cheap
- Don't use with changing dependencies: If deps change every render, memoization does nothing
- Don't forget dependencies: Missing deps cause stale closures
- Don't optimize prematurely: Build first, measure, then optimize
- Don't use for every callback: Only callbacks passed to memoized children
- Don't memoize state setters: They're already stable (setState from useState)
- Don't use without React.memo: useCallback alone doesn't prevent re-renders
- Don't sacrifice readability: If code becomes hard to understand, optimization isn't worth it
π‘ Pro Tips
- Start with useMemo, add useCallback as needed: Value optimization often matters more
- Use ESLint plugin: Install eslint-plugin-react-hooks for dependency warnings
- Profile in production mode: Development mode has extra overhead
- Consider code splitting first: Sometimes lazy loading helps more than memoization
- Virtualize long lists: Use libraries like react-window for huge lists
- Debounce user input: Combine with useMemo for search/filter scenarios
- Memoize context values: Prevents all consumers from re-rendering
- Use Web Workers for heavy calculations: Move expensive work off main thread
- Cache API responses: Combine with React Query or SWR for better performance
- Remember the cost: Each useMemo/useCallback has a small overhead
π― Optimization Priority
- First: Fix algorithm inefficiencies (O(nΒ²) β O(n))
- Second: Code splitting and lazy loading
- Third: Virtualization for long lists
- Fourth: Memoization with useMemo/useCallback
- Fifth: Web Workers for CPU-intensive tasks
Memoization is powerful, but it's not always the first solution!
β οΈ Common Mistakes to Avoid
Learn from these common pitfalls so you don't have to make them yourself!
Mistake 1: Missing Dependencies
// β Bad - missing dependency
function Component({ userId }: { userId: number }) {
const [data, setData] = useState(null);
const fetchData = useCallback(async () => {
const response = await fetch(`/api/user/${userId}`);
setData(await response.json());
}, []); // Missing userId!
// fetchData always uses the INITIAL userId value!
}
// β
Good - include all dependencies
function Component({ userId }: { userId: number }) {
const [data, setData] = useState(null);
const fetchData = useCallback(async () => {
const response = await fetch(`/api/user/${userId}`);
setData(await response.json());
}, [userId]); // Correct!
}
Mistake 2: Memoizing Everything
// β Bad - over-optimization
function Component({ name, age }: { name: string; age: number }) {
const greeting = useMemo(() => `Hello ${name}`, [name]); // Overkill!
const isAdult = useMemo(() => age >= 18, [age]); // Overkill!
const doubleAge = useMemo(() => age * 2, [age]); // Overkill!
return <div>{greeting} - Adult: {isAdult ? 'Yes' : 'No'}</div>;
}
// β
Good - keep it simple
function Component({ name, age }: { name: string; age: number }) {
const greeting = `Hello ${name}`; // Instant!
const isAdult = age >= 18; // Instant!
const doubleAge = age * 2; // Instant!
return <div>{greeting} - Adult: {isAdult ? 'Yes' : 'No'}</div>;
}
Mistake 3: Wrong Dependencies
// β Bad - dependency that always changes
function Component({ config }: { config: { theme: string } }) {
// config is a new object every render!
const theme = useMemo(() => {
return config.theme.toUpperCase();
}, [config]); // This recalculates every time anyway!
return <div>{theme}</div>;
}
// β
Good - depend on primitive value
function Component({ config }: { config: { theme: string } }) {
const theme = useMemo(() => {
return config.theme.toUpperCase();
}, [config.theme]); // Only recalculate when theme string changes
return <div>{theme}</div>;
}
Mistake 4: useCallback Without React.memo
// β Bad - useCallback is pointless here
const RegularChild = ({ onClick }: { onClick: () => void }) => {
return <button onClick={onClick}>Click</button>;
};
function Parent() {
const [count, setCount] = useState(0);
// Pointless - RegularChild re-renders anyway!
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
<RegularChild onClick={handleClick} />
</div>
);
}
// β
Good - useCallback with React.memo
const MemoizedChild = React.memo(({ onClick }: { onClick: () => void }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Now it prevents MemoizedChild from re-rendering!
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
<MemoizedChild onClick={handleClick} />
</div>
);
}
Mistake 5: Comparing Objects/Arrays
// β Bad - creating new array/object in dependencies
function Component({ items }: { items: number[] }) {
const processed = useMemo(() => {
return items.map(i => i * 2);
}, [items.filter(i => i > 0)]); // New array every time!
// This never uses the cached value!
}
// β
Good - stable dependencies
function Component({ items }: { items: number[] }) {
// Filter first, then memoize
const positiveItems = useMemo(() =>
items.filter(i => i > 0), [items]);
const processed = useMemo(() =>
positiveItems.map(i => i * 2), [positiveItems]);
return <div>{processed.join(', ')}</div>;
}
Mistake 6: Premature Optimization
// β Bad - optimizing before measuring
function SmallComponent() {
const [items] = useState([1, 2, 3, 4, 5]); // Only 5 items!
// Pointless optimization for tiny array
const doubled = useMemo(() =>
items.map(i => i * 2), [items]);
return <div>{doubled.join(', ')}</div>;
}
// β
Good - no optimization needed
function SmallComponent() {
const [items] = useState([1, 2, 3, 4, 5]);
// So cheap, just do it!
const doubled = items.map(i => i * 2);
return <div>{doubled.join(', ')}</div>;
}
β οΈ Remember
- Include ALL dependencies - ESLint will help you
- Don't optimize every function and value
- Depend on primitives when possible, not objects/arrays
- useCallback only helps with React.memo'd children
- Don't create new objects/arrays in dependency arrays
- Measure before you optimize - "premature optimization is the root of all evil"
π Summary
π Key Takeaways
- useMemo memoizes values - caches the result of expensive calculations
- useCallback memoizes functions - caches function references to prevent recreation
- Both prevent unnecessary recalculations - only recompute when dependencies change
- Memoization isn't free - it has memory and comparison costs
- Measure before optimizing - profile your app to find real bottlenecks
- Use for expensive operations - large lists, complex calculations, heavy processing
- useCallback needs React.memo - combine with memoized children for best results
- Dependencies matter - missing or wrong deps cause bugs
- Don't optimize everything - simple operations don't benefit from memoization
- TypeScript ensures type safety - catches dependency and type errors at compile time
π Quick Comparison
| Feature | useMemo | useCallback |
|---|---|---|
| Memoizes | Return value | Function reference |
| Returns | Computed value | Function itself |
| Use case | Expensive calculations | Stable callbacks |
| Best with | Heavy data processing | React.memo children |
| Example | Filter/sort 10,000 items | onClick handler for memoized child |
| Syntax | useMemo(() => value, [deps]) | useCallback(() => {}, [deps]) |
π― When to Use Decision Tree
π Key Patterns
Pattern 1: Filtering and Sorting
const filtered = useMemo(() =>
data.filter(predicate).sort(compareFn),
[data, predicate, compareFn]
);
Pattern 2: Complex Calculations
const stats = useMemo(() => ({
sum: data.reduce((a, b) => a + b, 0),
avg: data.reduce((a, b) => a + b) / data.length,
max: Math.max(...data)
}), [data]);
Pattern 3: Stable Callbacks for Memoized Children
const handleClick = useCallback((id: number) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
return <MemoizedChild onDelete={handleClick} />;
Pattern 4: Derived State
const activeItems = useMemo(() =>
items.filter(item => item.active),
[items]
);
const itemCount = useMemo(() =>
activeItems.length,
[activeItems]
);
π« Anti-Patterns to Avoid
- β Memoizing simple primitives:
useMemo(() => 2 + 2, []) - β Missing dependencies:
useMemo(() => data.filter(x => x > threshold), [data]) - β useCallback without React.memo child
- β Creating objects in dependency array:
[{ config }] - β Optimizing before measuring
- β Memoizing every single function and value
π What You've Learned
Congratulations! You now understand:
- β How React's rendering works and when performance matters
- β What memoization is and how it trades memory for speed
- β How to use useMemo to cache expensive calculations
- β How to use useCallback to memoize function references
- β The differences between useMemo and useCallback
- β When to use (and when NOT to use) these hooks
- β How to properly type memoized values with TypeScript
- β Real-world patterns for data tables, carts, search, and forms
- β How to profile and measure performance improvements
- β Best practices and common mistakes to avoid
π Additional Resources
- React useMemo Documentation
- React useCallback Documentation
- React.memo Documentation
- Kent C. Dodds - When to useMemo and useCallback
- Advanced Guide to React Performance
- Web.dev - Optimize React Performance
- Optimizing Performance - React Docs
π What's Next?
In the next lesson, we'll explore Compound Components Pattern - an advanced pattern for building flexible, reusable component APIs. You'll learn how to create component systems that work together seamlessly, sharing state implicitly while maintaining clean, intuitive interfaces. Perfect for building libraries like tabs, accordions, and dropdowns!
π― Practice Challenge
Before moving on, try building one of these projects to solidify your understanding:
- Optimized Data Grid: Build a sortable, filterable table with 10,000+ rows that stays smooth
- Real-Time Dashboard: Create a dashboard that processes streaming data without lag
- Advanced Search: Build a search with filters, facets, and instant results on 1000+ items
- Shopping Cart: Create a cart with complex calculations (tax, shipping, discounts) that updates instantly
- Performance Optimizer: Take an existing slow component and optimize it with useMemo/useCallback
π‘ Final Thoughts
Remember the golden rule: React is already fast! Most apps don't need heavy optimization. But when you do hit performance bottlenecks, useMemo and useCallback are powerful tools in your arsenal.
The key is knowing when to use them:
- β Do optimize when you measure a real problem
- β Do use for expensive operations on large datasets
- β Do combine with React.memo for maximum benefit
- β Don't optimize everything "just in case"
- β Don't use for simple, cheap operations
- β Don't sacrifice code clarity for micro-optimizations
Build first, measure second, optimize third. Happy coding! π
π Congratulations!
You've mastered React's performance optimization hooks! You now understand how to identify performance bottlenecks, measure their impact, and optimize strategically with useMemo and useCallback. This knowledge will help you build fast, responsive React applications that scale. Remember: premature optimization is the root of all evil, but smart optimization at the right time is pure gold. You've got the skills - now go build something amazing! πͺ