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
Explain React.memo, useMemo, and useCallback. What's the difference, and when should each be used?
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.