React.memo
Learn to use React.memo for preventing unnecessary re-renders of components. This is a foundational concept in component-based UI development that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world React experience. Take your time with each section and practice the examples
What is React.memo?
React.memo is a higher-order component that memoizes your component. It prevents re-rendering if the props haven't changed, which can improve performance.. This is an essential concept that every React developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
Basic React.memo Usage
import React from 'react';
// Without React.memo - re-renders on every parent update
function ExpensiveComponent({ data }) {
console.log('ExpensiveComponent rendered'); // This will log on every parent update
return (
<div>
<h2>Expensive Component</h2>
<p>{data}</p>
</div>
);
}
// With React.memo - only re-renders when props change
const MemoizedComponent = React.memo(function ExpensiveComponent({ data }) {
console.log('MemoizedComponent rendered'); // This will only log when data changes
return (
<div>
<h2>Memoized Component</h2>
<p>{data}</p>
</div>
);
});
// Parent component
function ParentComponent() {
const [count, setCount] = useState(0);
const [data, setData] = useState('Hello World');
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<MemoizedComponent data={data} />
</div>
);
}
// Real-world example: Product List
const ProductCard = React.memo(function ProductCard({ product, onAddToCart }) {
console.log(`ProductCard ${product.id} rendered`);
return (
<div className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price}</p>
<button onClick={() => onAddToCart(product)}>
Add to Cart
</button>
</div>
);
});
function ProductList({ products, onAddToCart }) {
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState('name');
return (
<div>
<div className="controls">
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter products..."
/>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Sort by Name</option>
<option value="price">Sort by Price</option>
</select>
</div>
<div className="products-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onAddToCart={onAddToCart}
/>
))}
</div>
</div>
);
}Custom Comparison Function
// Custom comparison function for React.memo
const UserProfile = React.memo(function UserProfile({ user, onUpdate }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={() => onUpdate(user.id)}>
Update User
</button>
</div>
);
}, (prevProps, nextProps) => {
// Custom comparison - only re-render if user data actually changed
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name &&
prevProps.user.email === nextProps.user.email
);
});
// Using the component
function UserList() {
const [users, setUsers] = useState([]);
const handleUpdate = useCallback((userId) => {
// Update logic
}, []);
return (
<div>
{users.map(user => (
<UserProfile
key={user.id}
user={user}
onUpdate={handleUpdate}
/>
))}
</div>
);
}Practice Exercise: Performance Optimization
// Exercise: Optimize a Data Visualization Dashboard
// Create a performance-optimized dashboard with charts and data tables
// src/components/DataTable.js
import React, { useMemo } from 'react';
const DataTable = React.memo(function DataTable({ data, columns, sortBy, sortDirection }) {
const sortedData = useMemo(() => {
if (!sortBy) return data;
return [...data].sort((a, b) => {
const aValue = a[sortBy];
const bValue = b[sortBy];
if (sortDirection === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
}, [data, sortBy, sortDirection]);
return (
<table className="data-table">
<thead>
<tr>
{columns.map(column => (
<th key={column.key}>{column.label}</th>
))}
</tr>
</thead>
<tbody>
{sortedData.map(row => (
<tr key={row.id}>
{columns.map(column => (
<td key={column.key}>{row[column.key]}</td>
))}
</tr>
))}
</tbody>
</table>
);
});
// src/components/Chart.js
const Chart = React.memo(function Chart({ data, type, width, height }) {
const chartData = useMemo(() => {
// Expensive chart data processing
return data.map(item => ({
label: item.label,
value: item.value,
percentage: (item.value / data.reduce((sum, d) => sum + d.value, 0)) * 100
}));
}, [data]);
return (
<div className="chart" style={{ width, height }}>
<h3>{type} Chart</h3>
<svg width={width} height={height}>
{chartData.map((item, index) => (
<rect
key={index}
x={index * (width / chartData.length)}
y={height - (item.percentage / 100) * height}
width={width / chartData.length - 2}
height={(item.percentage / 100) * height}
fill="#007bff"
/>
))}
</svg>
</div>
);
});
// src/components/Dashboard.js
import React, { useState, useCallback, useMemo } from 'react';
function Dashboard() {
const [data, setData] = useState([]);
const [filters, setFilters] = useState({});
const [sortConfig, setSortConfig] = useState({ sortBy: null, direction: 'asc' });
const [selectedChart, setSelectedChart] = useState('bar');
// Memoized filtered data
const filteredData = useMemo(() => {
return data.filter(item => {
return Object.entries(filters).every(([key, value]) => {
if (!value) return true;
return item[key].toLowerCase().includes(value.toLowerCase());
});
});
}, [data, filters]);
// Memoized chart data
const chartData = useMemo(() => {
return filteredData.reduce((acc, item) => {
const category = item.category;
if (!acc[category]) {
acc[category] = { label: category, value: 0 };
}
acc[category].value += item.value;
return acc;
}, {});
}, [filteredData]);
// Memoized callbacks
const handleSort = useCallback((column) => {
setSortConfig(prev => ({
sortBy: column,
direction: prev.sortBy === column && prev.direction === 'asc' ? 'desc' : 'asc'
}));
}, []);
const handleFilter = useCallback((key, value) => {
setFilters(prev => ({
...prev,
[key]: value
}));
}, []);
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'category', label: 'Category' },
{ key: 'value', label: 'Value' },
{ key: 'date', label: 'Date' }
];
return (
<div className="dashboard">
<div className="controls">
<input
placeholder="Filter by name..."
onChange={(e) => handleFilter('name', e.target.value)}
/>
<select onChange={(e) => handleFilter('category', e.target.value)}>
<option value="">All Categories</option>
<option value="sales">Sales</option>
<option value="marketing">Marketing</option>
<option value="development">Development</option>
</select>
<select value={selectedChart} onChange={(e) => setSelectedChart(e.target.value)}>
<option value="bar">Bar Chart</option>
<option value="line">Line Chart</option>
<option value="pie">Pie Chart</option>
</select>
</div>
<div className="dashboard-content">
<div className="charts-section">
<Chart
data={Object.values(chartData)}
type={selectedChart}
width={400}
height={300}
/>
</div>
<div className="table-section">
<DataTable
data={filteredData}
columns={columns}
sortBy={sortConfig.sortBy}
sortDirection={sortConfig.direction}
/>
</div>
</div>
</div>
);
}
// Performance monitoring
function PerformanceMonitor() {
const [renderCount, setRenderCount] = useState(0);
useEffect(() => {
setRenderCount(prev => prev + 1);
});
return (
<div className="performance-monitor">
<p>Component rendered {renderCount} times</p>
</div>
);
}
// Challenge: Add virtualization to the data table
// Challenge: Implement data caching with React Query
// Challenge: Add real-time data updates with WebSocket