React Lifecycle
Introduction
In React applications, components go through a series of stages from birth (mounting) to death (unmounting). This journey is known as the component lifecycle. Understanding these lifecycle phases and the methods associated with them is crucial for building efficient React applications.
The React component lifecycle allows you to run code at specific points during a component's existence. For example, you might want to fetch data when a component first appears, update something when props change, or clean up resources when a component disappears.
In this lesson, we'll explore:
- The main phases of React component lifecycle
- Lifecycle methods in class components
- How hooks handle lifecycle events in functional components
- Practical examples of when and how to use lifecycle features
Lifecycle Phases
React components go through three main phases during their existence:
Let's explore each phase in detail:
1. Mounting Phase
The mounting phase occurs when a component is being created and inserted into the DOM. This is where initialization happens.
2. Updating Phase
The updating phase happens when a component is being re-rendered due to changes in props or state.
3. Unmounting Phase
The unmounting phase occurs when a component is being removed from the DOM.
Lifecycle Methods in Class Components
Class components have specific methods that are called automatically during different lifecycle phases:
Let's look at these methods in detail:
Mounting Phase Methods
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = { date: new Date() };
console.log("Constructor called");
}
render() {
console.log("Render called");
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
componentDidMount() {
console.log("ComponentDidMount called");
this.timerID = setInterval(
() => this.tick(),
1000
);
}
tick() {
this.setState({
date: new Date()
});
}
}
In the console, you would see:
Constructor called
Render called
ComponentDidMount called
Constructor
- Called before anything else when the component is initialized
- Used for initializing state and binding methods
- You should call
super(props)
first to ensure proper initialization
render
- Required method in a class component
- Examines
this.props
andthis.state
- Returns React elements, arrays, portals, strings, numbers, booleans, or null
- Should be pure (doesn't modify component state)
componentDidMount
- Invoked immediately after a component is inserted into the DOM
- Perfect for:
- API calls
- DOM manipulation
- Setting up subscriptions
Updating Phase Methods
componentDidUpdate(prevProps, prevState) {
console.log("Component updated");
// Only fetch new data if the userId prop has changed
if (this.props.userId !== prevProps.userId) {
this.fetchUserData(this.props.userId);
}
}
componentDidUpdate
- Called after the component updates (re-renders)
- Not called for the initial render
- Perfect for:
- Network requests based on prop changes
- DOM manipulation after update
- Always compare current props/state with previous ones to avoid infinite loops
Unmounting Phase Methods
componentWillUnmount() {
console.log("Component will unmount");
// Clean up (e.g. removing event listeners, canceling network requests)
clearInterval(this.timerID);
}
componentWillUnmount
- Called right before the component is removed from the DOM
- Perfect for:
- Cleanup tasks
- Canceling network requests
- Removing event listeners
- Clearing timers
Lifecycle with Hooks in Functional Components
Modern React encourages the use of functional components with Hooks instead of class components. Hooks provide a more flexible way to use React features, including lifecycle events.
The useEffect Hook
useEffect
is the primary hook for handling lifecycle events in functional components:
import React, { useState, useEffect } from 'react';
function ClockFunctional() {
const [date, setDate] = useState(new Date());
// Similar to componentDidMount and componentDidUpdate
useEffect(() => {
console.log("Effect ran - similar to componentDidMount and componentDidUpdate");
const timerID = setInterval(() => {
setDate(new Date());
}, 1000);
// Similar to componentWillUnmount - cleanup function
return () => {
console.log("Cleanup - similar to componentWillUnmount");
clearInterval(timerID);
};
}, []); // Empty dependency array means this runs once on mount
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {date.toLocaleTimeString()}.</h2>
</div>
);
}
useEffect Dependency Array
The second argument to useEffect
- the dependency array - controls when the effect runs:
- Empty array
[]
: Run once after initial render (likecomponentDidMount
) - With dependencies
[prop1, state2]
: Run when any dependency changes - No dependency array: Run after every render
// Runs on every render
useEffect(() => {
console.log('The component rendered');
});
// Runs only on mount (first render)
useEffect(() => {
console.log('The component mounted');
}, []);
// Runs on mount and when 'count' changes
useEffect(() => {
console.log(`Count value: ${count}`);
}, [count]);
Practical Examples
1. Data Fetching with Lifecycle Methods
Class Component Approach:
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null,
loading: true,
error: null
};
}
componentDidMount() {
// Fetch user data when component mounts
fetch(`https://api.example.com/users/${this.props.userId}`)
.then(response => response.json())
.then(data => {
this.setState({ user: data, loading: false });
})
.catch(error => {
this.setState({ error, loading: false });
});
}
componentDidUpdate(prevProps) {
// Refetch when userId changes
if (prevProps.userId !== this.props.userId) {
this.setState({ loading: true });
fetch(`https://api.example.com/users/${this.props.userId}`)
.then(response => response.json())
.then(data => {
this.setState({ user: data, loading: false });
})
.catch(error => {
this.setState({ error, loading: false });
});
}
}
render() {
const { user, loading, error } = this.state;
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
}
Functional Component with Hooks:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// This effect runs on mount and when userId changes
setLoading(true);
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [userId]); // Dependency array with userId
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
2. Event Listeners and Cleanup
Event listeners should be cleaned up to prevent memory leaks:
Class Component:
class WindowResizeTracker extends React.Component {
constructor(props) {
super(props);
this.state = {
windowWidth: window.innerWidth
};
}
handleResize = () => {
this.setState({ windowWidth: window.innerWidth });
}
componentDidMount() {
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
// Clean up to avoid memory leaks
window.removeEventListener('resize', this.handleResize);
}
render() {
return <div>Window width: {this.state.windowWidth}px</div>;
}
}
Functional Component:
import React, { useState, useEffect } from 'react';
function WindowResizeTracker() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
// Add event listener
window.addEventListener('resize', handleResize);
// Cleanup function removes the event listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty dependency array means this runs once on mount
return <div>Window width: {windowWidth}px</div>;
}
Best Practices
-
Don't block rendering: Avoid time-consuming operations in the
constructor
orrender
methods. -
Use dependency arrays wisely: In
useEffect
, include all variables from the component scope that change over time and are used by the effect. -
Always clean up side effects: Whether using class or functional components, clean up subscriptions, timers, and listeners.
-
Don't update state in componentDidUpdate without a condition: Always use a condition to prevent infinite loops.
-
Data fetching patterns: Consider using a separate library like React Query or SWR for complex data fetching needs.
// Bad - will cause infinite loop
componentDidUpdate() {
this.setState({ counter: this.state.counter + 1 });
}
// Good - has a condition
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) {
this.setState({ counter: 0 });
}
}
Summary
React's component lifecycle provides hooks into different phases of a component's existence. Understanding these phases and their associated methods/hooks is essential for building efficient React applications.
- Class components use lifecycle methods like
componentDidMount
,componentDidUpdate
, andcomponentWillUnmount
. - Functional components use the
useEffect
hook to achieve the same functionality more flexibly. - The component lifecycle is crucial for tasks like:
- Data fetching
- DOM manipulation
- Event handling and cleanup
- Performance optimization
By mastering React's lifecycle, you'll be able to create more predictable, maintainable, and efficient React applications.
Additional Resources
- Practice building a data dashboard that fetches information when mounted and updates when filters change
- Create a component with cleanup needs, like a chat application that subscribes to messages
- Experiment with conditional rendering to observe mount and unmount behavior
- Explore debugging tools like React DevTools to observe component lifecycle in action
Next Steps
Now that you understand React's component lifecycle, you're ready to explore more advanced React concepts like Context API, advanced hooks like useReducer, and state management solutions like Redux.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)