React Lifecycle
Overview
The lifecycle of a React component is the complete process from creation to destruction. Understanding lifecycle is essential for optimizing component performance, handling side effects, and managing resources. This chapter will learn about the useEffect Hook for function components and lifecycle methods for class components.
🔄 Function Component Lifecycle (Hooks)
useEffect Basics
jsx
import React, { useState, useEffect } from 'react';
function BasicLifecycle() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Executes on both mount and update
useEffect(() => {
console.log('Component rendered');
document.title = `Count: ${count}`;
});
// Executes only once on mount
useEffect(() => {
console.log('Component mounted');
// Simulate API data fetch
fetch('/api/user')
.then(response => response.json())
.then(data => setName(data.name))
.catch(error => console.error('Failed to fetch data:', error));
}, []); // Empty dependency array
// Executes only when count changes
useEffect(() => {
console.log('Count changed:', count);
if (count > 5) {
alert('Count exceeded 5!');
}
}, [count]); // count dependency
return (
<div>
<h2>Lifecycle Demo</h2>
<p>Name: {name}</p>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment Count
</button>
</div>
);
}Cleanup Side Effects
jsx
function CleanupExample() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
let interval = null;
if (isRunning) {
console.log('Starting timer');
interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
}
// Cleanup function
return () => {
if (interval) {
console.log('Cleaning up timer');
clearInterval(interval);
}
};
}, [isRunning]);
// Cleanup on component unmount
useEffect(() => {
const handleBeforeUnload = () => {
console.log('Page is about to unload');
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
console.log('Removing event listener');
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, []);
const reset = () => {
setSeconds(0);
setIsRunning(false);
};
return (
<div>
<h2>Timer: {seconds}s</h2>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? 'Pause' : 'Start'}
</button>
<button onClick={reset}>Reset</button>
</div>
);
}Data Fetching and State Management
jsx
function DataFetching() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [page, setPage] = useState(1);
useEffect(() => {
let isCancelled = false;
const fetchUsers = async () => {
try {
setLoading(true);
setError(null);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await fetch(`/api/users?page=${page}`);
const data = await response.json();
// Check if component is still mounted
if (!isCancelled) {
setUsers(data.users);
setLoading(false);
}
} catch (err) {
if (! setError(errisCancelled) {
.message);
setLoading(false);
}
}
};
fetchUsers();
// Cleanup function to prevent memory leaks
return () => {
isCancelled = true;
};
}, [page]); // Dependencies: refetch when page changes
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error}</div>;
}
return (
<div>
<h2>User List</h2>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<button
onClick={() => setPage(page - 1)}
disabled={page <= 1}
>
Previous Page
</button>
<span> Page {page} </span>
<button onClick={() => setPage(page + 1)}>
Next Page
</button>
</div>
);
}🏗️ Class Component Lifecycle
Mounting Phase
jsx
class MountingLifecycle extends React.Component {
constructor(props) {
super(props);
console.log('1. constructor: Component constructor');
this.state = {
data: null,
loading: true
};
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log('2. getDerivedStateFromProps: Deriving state from props');
// Return new state or null
return null;
}
componentDidMount() {
console.log('4. componentDidMount: Component mount complete');
// Perform API calls, subscribe to events here
this.fetchData();
}
render() {
console.log('3. render: Rendering component');
const { data, loading } = this.state;
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h2>Class Component Mount Demo</h2>
<p>Data: {data}</p>
<button onClick={this.handleRefresh}>Refresh Data</button>
</div>
);
}
fetchData = async () => {
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
this.setState({
data: 'Data from server',
loading: false
});
} catch (error) {
console.error('Data fetch failed:', error);
this.setState({ loading: false });
}
};
handleRefresh = () => {
this.setState({ loading: true });
this.fetchData();
};
}Updating Phase
jsx
class UpdatingLifecycle extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
name: 'React'
};
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log('getDerivedStateFromProps: props changed');
return null;
}
shouldComponentUpdate(nextProps, nextState) {
console.log('shouldComponentUpdate: Should update?');
// Return false to prevent component update
// Here we prevent updates when count is even (demo only)
return nextState.count % 2 !== 0 || nextState.name !== this.state.name;
}
render() {
console.log('render: Re-rendering');
const { count, name } = this.state;
return (
<div>
<h2>Class Component Update Demo</h2>
<p>Count: {count}</p>
<p>Name: {name}</p>
<button onClick={this.incrementCount}>Increment Count</button>
<button onClick={this.changeName}>Change Name</button>
<p><small>Note: Even counts won't trigger view update</small></p>
</div>
);
}
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('getSnapshotBeforeUpdate: Snapshot before update');
// Return value will be passed to componentDidUpdate
if (prevState.count !== this.state.count) {
return { prevCount: prevState.count };
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('componentDidUpdate: Component update complete');
if (snapshot && snapshot.prevCount !== undefined) {
console.log(`Count updated from ${snapshot.prevCount} to ${this.state.count}`);
}
}
incrementCount = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
};
changeName = () => {
this.setState({
name: this.state.name === 'React' ? 'Vue' : 'React'
});
};
}Unmounting Phase
jsx
class UnmountingExample extends React.Component {
constructor(props) {
super(props);
this.state = { seconds: 0 };
this.interval = null;
}
componentDidMount() {
console.log('Component mounted, starting timer');
this.interval = setInterval(() => {
this.setState(prevState => ({ seconds: prevState.seconds + 1 }));
}, 1000);
// Add event listener
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
console.log('Component about to unmount, cleaning up resources');
// Cleanup timer
if (this.interval) {
clearInterval(this.interval);
}
// Remove event listener
window.removeEventListener('resize', this.handleResize);
// Cancel network requests
// this.abortController.abort();
}
handleResize = () => {
console.log('Window size changed');
};
render() {
return (
<div>
<h2>Elapsed time: {this.state.seconds}s</h2>
<p>Resize window to see event listener</p>
</div>
);
}
}
// Container component to demonstrate unmounting
function UnmountingContainer() {
const [showComponent, setShowComponent] = useState(true);
return (
<div>
<button onClick={() => setShowComponent(!showComponent)}>
{showComponent ? 'Unmount Component' : 'Mount Component'}
</button>
{showComponent && <UnmountingExample />}
</div>
);
}🔧 Custom Hooks
Encapsulating Lifecycle Logic
jsx
// Custom Hook: Component mount status
function useIsMounted() {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
}
// Custom Hook: Debounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Custom Hook: Data fetching
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const isMounted = useIsMounted();
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
const result = await response.json();
if (isMounted.current) {
setData(result);
}
} catch (err) {
if (isMounted.current) {
setError(err.message);
}
} finally {
if (isMounted.current) {
setLoading(false);
}
}
};
if (url) {
fetchData();
}
}, [url, isMounted]);
return { data, loading, error };
}
// Using custom Hooks
function CustomHooksExample() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const { data, loading, error } = useApi(
debouncedSearchTerm ? `/api/search?q=${debouncedSearchTerm}` : null
);
return (
<div>
<h2>Custom Hooks Demo</h2>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Enter search..."
style={{ padding: '8px', width: '200px' }}
/>
{loading && <p>Searching...</p>}
{error && <p>Error: {error}</p>}
{data && (
<div>
<h3>Search Results:</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)}
</div>
);
}🎯 Lifecycle Best Practices
Performance Optimization
jsx
function PerformanceOptimization() {
const [posts, setPosts] = useState([]);
const [currentUser, setCurrentUser] = useState(null);
const [theme, setTheme] = useState('light');
// Avoid creating new objects on each render
const memoizedUser = useMemo(() => {
return currentUser ? {
...currentUser,
fullName: `${currentUser.firstName} ${currentUser.lastName}`
} : null;
}, [currentUser]);
// Cache expensive computations
const expensiveValue = useMemo(() => {
console.log('Running expensive computation');
return posts.reduce((total, post) => total + post.likes, 0);
}, [posts]);
// Cache callback functions
const handlePostLike = useCallback((postId) => {
setPosts(posts => posts.map(post =>
post.id === postId
? { ...post, likes: post.likes + 1 }
: post
));
}, []);
// Fetch data
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await fetch('/api/posts');
const data = await response.json();
setPosts(data);
} catch (error) {
console.error('Failed to fetch posts:', error);
}
};
fetchPosts();
}, []);
return (
<div className={`app-${theme}`}>
<h2>Performance Optimization Demo</h2>
{memoizedUser && (
<div>Welcome, {memoizedUser.fullName}!</div>
)}
<div>Total Likes: {expensiveValue}</div>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
<PostList posts={posts} onLike={handlePostLike} />
</div>
);
}
// Optimized child component
const PostList = React.memo(function PostList({ posts, onLike }) {
console.log('PostList rendered');
return (
<div>
{posts.map(post => (
<PostItem key={post.id} post={post} onLike={onLike} />
))}
</div>
);
});
const PostItem = React.memo(function PostItem({ post, onLike }) {
console.log('PostItem rendered:', post.title);
return (
<div style={{ border: '1px solid #ddd', padding: '10px', margin: '10px 0' }}>
<h3>{post.title}</h3>
<p>Likes: {post.likes}</p>
<button onClick={() => onLike(post.id)}>
👍 Like
</button>
</div>
);
});Error Boundaries
jsx
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// Update state to show error UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error boundary caught error:', error, errorInfo);
// Can send error to error monitoring service
// logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px', border: '1px solid red', margin: '10px' }}>
<h2>An Error Occurred</h2>
<p>The application encountered an error, please refresh the page to retry.</p>
<details>
<summary>Error Details</summary>
<pre>{this.state.error && this.state.error.toString()}</pre>
</details>
<button onClick={() => window.location.reload()}>
Refresh Page
</button>
</div>
);
}
return this.props.children;
}
}
// Component that might throw an error
function BuggyComponent() {
const [count, setCount] = useState(0);
if (count > 3) {
throw new Error('Counter exploded!');
}
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment Count (will error after 3)
</button>
</div>
);
}
// Using error boundary
function ErrorBoundaryExample() {
return (
<div>
<h2>Error Boundary Demo</h2>
<ErrorBoundary>
<BuggyComponent />
</ErrorBoundary>
</div>
);
}📝 Chapter Summary
Through this chapter, you should have mastered:
Function Component Lifecycle
- ✅ useEffect Hook usage
- ✅ Dependency array role and best practices
- ✅ Cleanup side effects to prevent memory leaks
- ✅ Custom Hooks to encapsulate lifecycle logic
Class Component Lifecycle
- ✅ Three phases: Mount, Update, Unmount
- ✅ Purpose of each lifecycle method
- ✅ Performance optimization and error handling
- ✅ Migration from class components to function components
Best Practices
- Resource cleanup: Clean up timers and event listeners in a timely manner
- Dependency optimization: Set useEffect dependencies correctly
- Performance monitoring: Use React DevTools Profiler
- Error handling: Implement error boundary components
- Code organization: Use custom Hooks to reuse logic
Continue Learning: Next Chapter - React Component Communication