Component Lifecycle & Performance
Introduction to Component Lifecycle
Understanding the React component lifecycle and optimization techniques is crucial for building performant applications. This lesson covers lifecycle concepts in modern React, performance optimization with React.memo, useMemo, useCallback, and profiling tools.
Component Lifecycle in Functional Components
Functional components use hooks to handle lifecycle events. The useEffect hook replaces class component lifecycle methods:
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// ComponentDidMount + ComponentDidUpdate (when userId changes)
useEffect(() => {
console.log('Component mounted or userId changed');
setLoading(true);
fetch(`https://api.example.com/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
// ComponentWillUnmount (cleanup function)
return () => {
console.log('Cleanup before re-running effect or unmounting');
};
}, [userId]); // Dependency array - effect runs when userId changes
// Runs only on mount (empty dependency array)
useEffect(() => {
console.log('Component mounted once');
document.title = `User Profile - ${userId}`;
return () => {
console.log('Component unmounting');
document.title = 'React App';
};
}, []); // Empty array = mount/unmount only
// Runs on every render (no dependency array - avoid this!)
useEffect(() => {
console.log('Runs after every render');
});
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Lifecycle Equivalents:
useEffect(() => {}, [])= componentDidMountuseEffect(() => {})= componentDidMount + componentDidUpdatereturn () => {}in useEffect = componentWillUnmount
React.memo: Preventing Unnecessary Re-renders
React.memo is a higher-order component that memoizes component output, preventing re-renders when props haven't changed:
import { memo } from 'react';
// Without memo - re-renders every time parent re-renders
function ExpensiveComponent({ data, count }) {
console.log('ExpensiveComponent rendered');
// Imagine expensive calculations here
const processedData = data.map(item => ({
...item,
calculated: item.value * 1000
}));
return (
<div>
<h3>Count: {count}</h3>
<ul>
{processedData.map(item => (
<li key={item.id}>{item.calculated}</li>
))}
</ul>
</div>
);
}
// With memo - only re-renders when props change
const MemoizedExpensiveComponent = memo(ExpensiveComponent);
// Usage
function ParentComponent() {
const [count, setCount] = useState(0);
const [otherState, setOtherState] = useState(0);
const data = [
{ id: 1, value: 10 },
{ id: 2, value: 20 },
{ id: 3, value: 30 }
];
return (
<div>
{/* This will re-render only when count or data changes */}
<MemoizedExpensiveComponent data={data} count={count} />
<button onClick={() => setCount(c => c + 1)}>
Increment Count
</button>
{/* This won't trigger MemoizedExpensiveComponent re-render */}
<button onClick={() => setOtherState(s => s + 1)}>
Update Other State ({otherState})
</button>
</div>
);
}
When to use React.memo: Use it for components that render often with the same props, especially if they perform expensive calculations or render large lists.
Custom Comparison Function with React.memo
You can provide a custom comparison function for complex prop comparisons:
import { memo } from 'react';
function UserCard({ user, onEdit }) {
console.log('UserCard rendered');
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
}
// Custom comparison: only re-render if user.id or user.name changed
const MemoizedUserCard = memo(UserCard, (prevProps, nextProps) => {
// Return true if props are equal (skip render)
// Return false if props are different (re-render)
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name
// Note: onEdit function reference changes won't trigger re-render
);
});
export default MemoizedUserCard;
Warning: React.memo uses shallow comparison by default. For objects and arrays, provide a custom comparison function or ensure reference stability.
useMemo: Memoizing Expensive Calculations
useMemo caches the result of expensive calculations between renders:
import { useState, useMemo } from 'react';
function ProductList({ products, filter }) {
const [sortOrder, setSortOrder] = useState('asc');
// Without useMemo - recalculated on every render
const filteredProducts = products
.filter(p => p.category === filter)
.sort((a, b) => {
return sortOrder === 'asc'
? a.price - b.price
: b.price - a.price;
});
// With useMemo - only recalculated when dependencies change
const memoizedFilteredProducts = useMemo(() => {
console.log('Recalculating filtered products');
return products
.filter(p => p.category === filter)
.sort((a, b) => {
return sortOrder === 'asc'
? a.price - b.price
: b.price - a.price;
});
}, [products, filter, sortOrder]); // Dependencies
return (
<div>
<button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
Sort: {sortOrder}
</button>
<ul>
{memoizedFilteredProducts.map(product => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
</div>
);
}
// More useMemo examples
function AnalyticsDashboard({ data }) {
// Expensive calculations
const statistics = useMemo(() => {
console.log('Calculating statistics...');
const total = data.reduce((sum, item) => sum + item.value, 0);
const average = total / data.length;
const max = Math.max(...data.map(d => d.value));
const min = Math.min(...data.map(d => d.value));
return { total, average, max, min };
}, [data]);
return (
<div>
<p>Total: {statistics.total}</p>
<p>Average: {statistics.average.toFixed(2)}</p>
<p>Max: {statistics.max}</p>
<p>Min: {statistics.min}</p>
</div>
);
}
Note: Don't overuse useMemo. The memoization itself has overhead. Only use it for genuinely expensive calculations or to maintain reference equality.
useCallback: Memoizing Functions
useCallback returns a memoized callback function, preventing unnecessary re-renders of child components:
import { useState, useCallback, memo } from 'react';
// Child component that receives a callback
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
console.log(`TodoItem ${todo.id} rendered`);
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
});
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build Project', completed: false },
{ id: 3, text: 'Deploy App', completed: false }
]);
const [filter, setFilter] = useState('all');
// Without useCallback - new function on every render
// This would cause TodoItem to re-render even with React.memo
const handleToggle = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
// With useCallback - same function reference between renders
const handleToggleMemoized = useCallback((id) => {
setTodos(prevTodos => prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []); // Empty deps - function never changes
const handleDelete = useCallback((id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
}, []);
// Function with dependencies
const handleAddTodo = useCallback((text) => {
const newTodo = {
id: Date.now(),
text,
completed: false
};
setTodos(prevTodos => [...prevTodos, newTodo]);
}, []); // Could have dependencies if needed
return (
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('completed')}>Completed</button>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggleMemoized}
onDelete={handleDelete}
/>
))}
</div>
);
}
Best Practice: Use useCallback when passing callbacks to memoized child components. Without it, child components will re-render because the callback reference changes.
Combining Optimization Techniques
Real-world example combining React.memo, useMemo, and useCallback:
import { useState, useMemo, useCallback, memo } from 'react';
// Memoized child component
const DataRow = memo(({ item, onUpdate, onDelete }) => {
console.log(`Row ${item.id} rendered`);
return (
<tr>
<td>{item.name}</td>
<td>{item.value}</td>
<td>
<button onClick={() => onUpdate(item.id, item.value + 1)}>+</button>
<button onClick={() => onDelete(item.id)}>Delete</button>
</td>
</tr>
);
});
function OptimizedDataTable({ initialData }) {
const [data, setData] = useState(initialData);
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState('name');
// Memoized filtered and sorted data
const processedData = useMemo(() => {
console.log('Processing data...');
let result = data;
// Filter
if (filter) {
result = result.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}
// Sort
result = [...result].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'value') return a.value - b.value;
return 0;
});
return result;
}, [data, filter, sortBy]);
// Memoized callbacks
const handleUpdate = useCallback((id, newValue) => {
setData(prevData =>
prevData.map(item =>
item.id === id ? { ...item, value: newValue } : item
)
);
}, []);
const handleDelete = useCallback((id) => {
setData(prevData => prevData.filter(item => item.id !== id));
}, []);
return (
<div>
<input
type="text"
placeholder="Filter by name..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Sort by Name</option>
<option value="value">Sort by Value</option>
</select>
<table>
<tbody>
{processedData.map(item => (
<DataRow
key={item.id}
item={item}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>
))}
</tbody>
</table>
</div>
);
}
React DevTools Profiler
Use React DevTools Profiler to identify performance bottlenecks:
// Install React DevTools browser extension
// Chrome: https://chrome.google.com/webstore (search "React Developer Tools")
// Firefox: https://addons.mozilla.org/firefox/ (search "React Developer Tools")
// Using the Profiler programmatically
import { Profiler } from 'react';
function onRenderCallback(
id, // Component identifier
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime,
commitTime
) {
console.log(`Component ${id} - ${phase}`, {
actualDuration,
baseDuration,
improvement: baseDuration - actualDuration
});
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<TodoList />
<DataTable />
</Profiler>
);
}
Exercise 1: Optimize Product List
Task: Optimize a product list component:
- Create a list of 100+ products with search and filter
- Use
React.memoon the ProductCard component - Use
useMemoto memoize filtered/sorted results - Use
useCallbackfor event handlers - Measure performance improvement with React DevTools Profiler
Exercise 2: Lifecycle Management
Task: Build a timer component with proper cleanup:
- Create a countdown timer that updates every second
- Use
useEffectwith proper cleanup (clear interval on unmount) - Add pause/resume functionality
- Prevent memory leaks when component unmounts
- Add document title update showing remaining time
Exercise 3: Complex Data Dashboard
Task: Build an optimized analytics dashboard:
- Display charts with expensive calculations (mean, median, standard deviation)
- Use
useMemofor statistical calculations - Implement filters and date range selectors
- Memoize child chart components with
React.memo - Compare performance before and after optimization
Summary
In this lesson, you learned essential performance optimization techniques:
- Component lifecycle in functional components with
useEffect - Preventing unnecessary re-renders with
React.memo - Memoizing expensive calculations with
useMemo - Memoizing callback functions with
useCallback - Combining optimization techniques for maximum performance
- Profiling and measuring performance with React DevTools
- Best practices for when and how to apply optimizations
In the next lesson, we'll explore error boundaries, Suspense, and advanced error handling patterns in React.