Explain React.memo, useMemo, and useCallback. What's the difference, and when should each be used?
Quick Answer
React provides three main optimization hooks/methods to prevent unnecessary re-renders and expensive calculations. Understanding when and how to use each is crucial for building performant React applications.
Detailed Answer
React provides three main optimization hooks/methods to prevent unnecessary re-renders and expensive calculations. Understanding when and how to use each is crucial for building performant React applications.
React.memo - Component Memoization
React.memo is a higher-order component that memoizes the result of a component. It only re-renders when its props change.
// Basic usage
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
console.log('ExpensiveComponent rendered');
return (
<div>
<h3>Data: {data.title}</h3>
<button onClick={() => onUpdate(data.id)}>Update</button>
</div>
);
});
// Custom comparison function
const UserCard = React.memo(({ user, onEdit }) => {
return (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
}, (prevProps, nextProps) => {
// Only re-render if specific properties change
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.user.email === nextProps.user.email &&
prevProps.user.avatar === nextProps.user.avatar
);
});
// Usage
function UserList({ users, onUserEdit }) {
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onEdit={onUserEdit}
/>
))}
</div>
);
}
useMemo - Value Memoization
useMemo memoizes the result of a computation and only recalculates when dependencies change.
function DataVisualization({ data, filters, sortBy }) {
// Expensive calculation - only runs when data, filters, or sortBy changes
const processedData = useMemo(() => {
console.log('Processing data...');
return data
.filter(item => {
if (filters.category !== 'all' && item.category !== filters.category) {
return false;
}
if (filters.status !== 'all' && item.status !== filters.status) {
return false;
}
return true;
})
.map(item => ({
...item,
score: calculateComplexScore(item),
trend: calculateTrend(item.history),
normalizedValue: normalizeValue(item.value, data)
}))
.sort((a, b) => {
switch (sortBy) {
case 'score':
return b.score - a.score;
case 'name':
return a.name.localeCompare(b.name);
case 'date':
return new Date(b.date) - new Date(a.date);
default:
return 0;
}
});
}, [data, filters.category, filters.status, sortBy]);
// Derived calculations - only recalculates when processedData changes
const statistics = useMemo(() => ({
total: processedData.length,
average: processedData.reduce((sum, item) => sum + item.score, 0) / processedData.length,
topPerformer: processedData[0]?.name || 'N/A',
categories: [...new Set(processedData.map(item => item.category))],
scoreDistribution: calculateScoreDistribution(processedData)
}), [processedData]);
// Expensive object creation - memoized to prevent child re-renders
const chartConfig = useMemo(() => ({
data: processedData,
options: {
responsive: true,
plugins: {
legend: { position: 'top' },
title: { display: true, text: 'Data Visualization' }
},
scales: {
y: { beginAtZero: true },
x: { type: 'category' }
}
}
}), [processedData]);
return (
<div>
<StatisticsDisplay stats={statistics} />
<Chart config={chartConfig} />
<DataTable data={processedData} />
</div>
);
}
useCallback - Function Memoization
useCallback memoizes a function and only recreates it when dependencies change.
function UserManagement() {
const [users, setUsers] = useState<User[]>([]);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
// Memoize event handlers to prevent child re-renders
const handleUserSelect = useCallback((userId: string) => {
setSelectedUserId(userId);
}, []);
const handleUserUpdate = useCallback((userId: string, updates: Partial<User>) => {
setUsers(prevUsers =>
prevUsers.map(user =>
user.id === userId ? { ...user, ...updates } : user
)
);
}, []);
const handleUserDelete = useCallback((userId: string) => {
setUsers(prevUsers => prevUsers.filter(user => user.id !== userId));
if (selectedUserId === userId) {
setSelectedUserId(null);
}
}, [selectedUserId]);
const handleSearch = useCallback((query: string) => {
setSearchQuery(query);
}, []);
// Memoize filtered users
const filteredUsers = useMemo(() => {
if (!searchQuery) return users;
return users.filter(user =>
user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [users, searchQuery]);
// Memoize sorted users
const sortedUsers = useMemo(() => {
return [...filteredUsers].sort((a, b) => a.name.localeCompare(b.name));
}, [filteredUsers]);
return (
<div>
<SearchInput onSearch={handleSearch} />
<UserList
users={sortedUsers}
onUserSelect={handleUserSelect}
onUserUpdate={handleUserUpdate}
onUserDelete={handleUserDelete}
/>
{selectedUserId && (
<UserDetails
userId={selectedUserId}
onUpdate={handleUserUpdate}
/>
)}
</div>
);
}
Key Differences and When to Use Each:
| Hook/Method | Purpose | When to Use | Dependencies |
|---|---|---|---|
React.memo | Prevents component re-renders | When component receives same props frequently | Props comparison |
useMemo | Memoizes expensive calculations | Expensive computations, object/array creation | Value dependencies |
useCallback | Memoizes function references | Event handlers, functions passed as props | Function dependencies |
Detailed Comparison:
- React.memo vs useMemo:
// React.memo - prevents component re-renders
const ExpensiveComponent = React.memo(({ data }) => {
// This component only re-renders when 'data' prop changes
return <div>{data.title}</div>;
});
// useMemo - prevents expensive calculations
function ParentComponent({ data }) {
const expensiveValue = useMemo(() => {
// This calculation only runs when 'data' changes
return data.reduce((sum, item) => sum + item.value, 0);
}, [data]);
return <div>{expensiveValue}</div>;
}
- useCallback vs useMemo:
function Example() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// useMemo - memoizes a value
const expensiveValue = useMemo(() => {
return count * 1000; // Expensive calculation
}, [count]);
// useCallback - memoizes a function
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
return (
<div>
<p>Value: {expensiveValue}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
Advanced Patterns:
- Combining All Three:
// Memoized component
const UserCard = React.memo(({ user, onEdit, onDelete }) => {
// Memoized expensive calculation
const userStats = useMemo(() => {
return {
activityScore: calculateActivityScore(user),
riskLevel: assessRiskLevel(user),
recommendations: generateRecommendations(user)
};
}, [user.id, user.lastActivity, user.transactions]);
// Memoized event handlers
const handleEdit = useCallback(() => {
onEdit(user.id);
}, [user.id, onEdit]);
const handleDelete = useCallback(() => {
onDelete(user.id);
}, [user.id, onDelete]);
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>Activity Score: {userStats.activityScore}</p>
<p>Risk Level: {userStats.riskLevel}</p>
<button onClick={handleEdit}>Edit</button>
<button onClick={handleDelete}>Delete</button>
</div>
);
});
// Parent component with stable references
function UserList({ users, onUserEdit, onUserDelete }) {
const handleUserEdit = useCallback((userId: string) => {
onUserEdit(userId);
}, [onUserEdit]);
const handleUserDelete = useCallback((userId: string) => {
onUserDelete(userId);
}, [onUserDelete]);
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onEdit={handleUserEdit}
onDelete={handleUserDelete}
/>
))}
</div>
);
}
- Custom Comparison Functions:
// Deep comparison for complex objects
const DeepMemoComponent = React.memo(({ config, data }) => {
return <ComplexVisualization config={config} data={data} />;
}, (prevProps, nextProps) => {
// Custom deep comparison
return (
JSON.stringify(prevProps.config) === JSON.stringify(nextProps.config) &&
prevProps.data.length === nextProps.data.length &&
prevProps.data.every((item, index) =>
JSON.stringify(item) === JSON.stringify(nextProps.data[index])
)
);
});
// Shallow comparison for specific properties
const SelectiveMemoComponent = React.memo(({ user, settings, onUpdate }) => {
return <UserProfile user={user} settings={settings} onUpdate={onUpdate} />;
}, (prevProps, nextProps) => {
// Only compare specific properties
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.settings.theme === nextProps.settings.theme
// Ignore other properties
);
});
- Conditional Memoization:
function ConditionalComponent({ data, shouldOptimize }) {
// Only memoize when optimization is enabled
const processedData = shouldOptimize
? useMemo(() => expensiveProcessing(data), [data])
: expensiveProcessing(data);
return <div>{processedData}</div>;
}
// Or with custom hook
function useConditionalMemo<T>(factory: () => T, deps: any[], condition: boolean): T {
return condition ? useMemo(factory, deps) : factory();
}
Common Pitfalls and Best Practices:
- Don't Over-Memoize:
// BAD - Unnecessary memoization
function SimpleComponent({ name }) {
const memoizedName = useMemo(() => name, [name]); // Unnecessary!
return <div>{memoizedName}</div>;
}
// GOOD - Only memoize when needed
function SimpleComponent({ name }) {
return <div>{name}</div>; // Simple values don't need memoization
}
- Include All Dependencies:
// BAD - Missing dependencies
function BadExample({ userId, filters }) {
const data = useMemo(() => {
return fetchData(userId, filters); // filters not in deps!
}, [userId]);
return <div>{data}</div>;
}
// GOOD - Include all dependencies
function GoodExample({ userId, filters }) {
const data = useMemo(() => {
return fetchData(userId, filters);
}, [userId, filters]);
return <div>{data}</div>;
}
- Use Stable References:
// BAD - New object on every render
function BadParent() {
const config = { theme: 'dark', size: 'large' }; // New object every time!
return <ChildComponent config={config} />;
}
// GOOD - Memoized object
function GoodParent() {
const config = useMemo(() => ({
theme: 'dark',
size: 'large'
}), []);
return <ChildComponent config={config} />;
}
Coding Challenge: Identify and fix performance issues in a provided code sample with excessive re-renders.
Problem Code:
function UserDashboard({ userId }) {
const [users, setUsers] = useState([]);
const [selectedUser, setSelectedUser] = useState(null);
const [filters, setFilters] = useState({ status: 'all', role: 'all' });
// Problem 1: Expensive calculation runs on every render
const processedUsers = users.map(user => ({
...user,
score: calculateUserScore(user),
risk: assessRisk(user),
recommendations: generateRecommendations(user)
}));
// Problem 2: New object created on every render
const chartData = {
users: processedUsers,
config: { type: 'bar', colors: ['blue', 'green'] }
};
// Problem 3: New function created on every render
const handleUserSelect = (user) => {
setSelectedUser(user);
};
// Problem 4: New function created on every render
const handleFilterChange = (newFilters) => {
setFilters(newFilters);
};
// Problem 5: Expensive filtering runs on every render
const filteredUsers = processedUsers.filter(user => {
if (filters.status !== 'all' && user.status !== filters.status) return false;
if (filters.role !== 'all' && user.role !== filters.role) return false;
return true;
});
return (
<div>
<FilterPanel filters={filters} onFilterChange={handleFilterChange} />
<UserChart data={chartData} />
<UserList
users={filteredUsers}
onUserSelect={handleUserSelect}
/>
{selectedUser && <UserDetails user={selectedUser} />}
</div>
);
}
Optimized Solution:
function UserDashboard({ userId }) {
const [users, setUsers] = useState([]);
const [selectedUser, setSelectedUser] = useState(null);
const [filters, setFilters] = useState({ status: 'all', role: 'all' });
// Fix 1: Memoize expensive calculations
const processedUsers = useMemo(() => {
return users.map(user => ({
...user,
score: calculateUserScore(user),
risk: assessRisk(user),
recommendations: generateRecommendations(user)
}));
}, [users]);
// Fix 2: Memoize filtered users
const filteredUsers = useMemo(() => {
return processedUsers.filter(user => {
if (filters.status !== 'all' && user.status !== filters.status) return false;
if (filters.role !== 'all' && user.role !== filters.role) return false;
return true;
});
}, [processedUsers, filters.status, filters.role]);
// Fix 3: Memoize chart data object
const chartData = useMemo(() => ({
users: filteredUsers,
config: { type: 'bar', colors: ['blue', 'green'] }
}), [filteredUsers]);
// Fix 4: Memoize event handlers
const handleUserSelect = useCallback((user) => {
setSelectedUser(user);
}, []);
const handleFilterChange = useCallback((newFilters) => {
setFilters(newFilters);
}, []);
return (
<div>
<FilterPanel filters={filters} onFilterChange={handleFilterChange} />
<UserChart data={chartData} />
<UserList
users={filteredUsers}
onUserSelect={handleUserSelect}
/>
{selectedUser && <UserDetails user={selectedUser} />}
</div>
);
}
// Fix 5: Memoize child components
const UserList = React.memo(({ users, onUserSelect }) => {
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onSelect={onUserSelect}
/>
))}
</div>
);
});
const UserCard = React.memo(({ user, onSelect }) => {
const handleClick = useCallback(() => {
onSelect(user);
}, [user, onSelect]);
return (
<div onClick={handleClick}>
<h3>{user.name}</h3>
<p>Score: {user.score}</p>
<p>Risk: {user.risk}</p>
</div>
);
});
Performance Monitoring:
// Custom hook to measure render performance
function useRenderPerformance(componentName: string) {
const renderCount = useRef(0);
const startTime = useRef(performance.now());
renderCount.current++;
useEffect(() => {
const endTime = performance.now();
const renderTime = endTime - startTime.current;
console.log(`${componentName} rendered ${renderCount.current} times in ${renderTime.toFixed(2)}ms`);
startTime.current = performance.now();
});
}
// Usage
function OptimizedComponent() {
useRenderPerformance('OptimizedComponent');
return <div>Component content</div>;
}
Key Takeaways:
- React.memo: Use for components that receive the same props frequently
- useMemo: Use for expensive calculations and object/array creation
- useCallback: Use for functions passed as props to prevent child re-renders
- Measure First: Use React DevTools Profiler to identify actual bottlenecks
- Don't Over-Optimize: Only optimize when you have performance issues
- Include Dependencies: Always include all dependencies in dependency arrays
- Stable References: Ensure objects and functions have stable references
The key is to understand that these optimizations come with a cost (memory usage, complexity) and should only be used when they provide actual performance benefits.