React Lifecycle Methods: Mounting, Updating, Unmounting
React class component lifecycle methods allow you to hook into specific phases—mounting, updating, and unmounting—to run side effects like data fetching, subscriptions, and cleanup. The three core methods (componentDidMount, componentDidUpdate, componentWillUnmount) give you precise timing control that modern functional components replicate with the useEffect hook.
Key Takeaways
- Lifecycle has three phases: Mounting (component creation), Updating (props/state change), Unmounting (DOM removal)
componentDidMountfires once after first render—ideal for API calls, subscriptions, or DOM manipulationcomponentDidUpdate(prevProps, prevState)fires after every re-render; compare previous props to avoid infinite loopscomponentWillUnmountexecutes before removal—cleanup timers, subscriptions, and pending requests to prevent memory leaks- Modern alternative: Functional components use
useEffecthook instead of lifecycle methods (preferred in new code)
Understanding the Component Lifecycle
Every React component progresses through distinct lifecycle phases. Lifecycle methods are special functions React calls automatically at each stage. Think of it as the component being "born" (mounting), "living" (updating), and "dying" (unmounting).
The three main phases are:
- Mounting: Component instance is created and inserted into the DOM.
- Updating: Component re-renders due to prop or state changes.
- Unmounting: Component is removed from the DOM.
What Is componentDidMount() and When Should You Use It?
componentDidMount() is called once, immediately after the component renders to the DOM for the first time. This is the ideal place for tasks requiring a DOM node, such as fetching data from an API, initializing third-party libraries, or setting up subscriptions. In modern React, useEffect(() => { ... }, []) (empty dependency array) replaces this method.
// components/DataFetcher.jsx
import React, { Component } from 'react';
class DataFetcher extends Component {
constructor(props) {
super(props);
this.state = { data: null, loading: true };
}
componentDidMount() {
// Called once after first render
console.log('Component has mounted!');
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => this.setState({ data, loading: false }));
}
render() {
if (this.state.loading) {
return <div>Loading...</div>;
}
return <div>Data: {JSON.stringify(this.state.data)}</div>;
}
}
export default DataFetcher;
Why it matters: Fetching in componentDidMount ensures the component is mounted before state updates. Fetching in the constructor or render() causes errors or infinite loops. This method guarantees a single fetch on mount, not on every re-render.
How Does componentDidUpdate() Detect Prop Changes?
componentDidUpdate(prevProps, prevState) is called immediately after a re-render—but not on initial mount. It receives the previous props and state as arguments, letting you compare them to the current values. This is critical for making conditional API calls without creating infinite loops.
// components/Logger.jsx
import React, { Component } from 'react';
class Logger extends Component {
componentDidUpdate(prevProps, prevState) {
console.log('Component updated!');
console.log('Previous props:', prevProps);
console.log('Current props:', this.props);
// Example: Fetch new data only if userId prop changed
if (this.props.userId !== prevProps.userId) {
console.log('User ID changed, fetching new data...');
this.fetchUserData(this.props.userId);
}
}
fetchUserData(userId) {
// Simulate API call
console.log('Fetching data for user:', userId);
}
render() {
return <div>Current user ID: {this.props.userId}</div>;
}
}
export default Logger;
Why it matters: Without comparing prevProps to this.props, any API call in componentDidUpdate runs after every render, wasting bandwidth and causing performance problems. The comparison pattern prevents unnecessary work while still reacting to important changes.
Why Must You Clean Up in componentWillUnmount()?
componentWillUnmount() executes immediately before a component is removed from the DOM. It is your only opportunity to clean up resources (timers, subscriptions, pending network requests). Forgetting cleanup is the #1 cause of memory leaks in class components.
// components/Timer.jsx
import React, { Component } from 'react';
class Timer extends Component {
constructor(props) {
super(props);
this.state = { time: new Date() };
}
componentDidMount() {
// Set up interval
this.timerId = setInterval(() => {
this.setState({ time: new Date() });
}, 1000);
}
componentWillUnmount() {
// Clean up: stop the interval
console.log('Cleaning up timer!');
clearInterval(this.timerId);
}
render() {
return <div>Current time: {this.state.time.toLocaleTimeString()}</div>;
}
}
export default Timer;
Why it matters: If you set up an interval but forget to clear it on unmount, the interval continues running and calls setState on a destroyed component. React prints a warning and your app wastes CPU and memory. This pattern applies to all subscriptions, event listeners, and timers.
Complete Example: Fetching User Data with Cleanup
Here's a realistic example combining all three lifecycle methods: fetch user data on mount, refetch if a prop changes, and cancel any pending request on unmount.
// components/UserProfile.jsx
import React, { Component } from 'react';
class UserProfile extends Component {
constructor(props) {
super(props);
this.state = { user: null, loading: true, error: null };
this.abortController = null;
}
componentDidMount() {
this.fetchUser(this.props.userId);
}
componentDidUpdate(prevProps) {
// Refetch if userId changed
if (this.props.userId !== prevProps.userId) {
this.fetchUser(this.props.userId);
}
}
componentWillUnmount() {
// Cancel pending request
if (this.abortController) {
this.abortController.abort();
}
}
fetchUser = (userId) => {
this.abortController = new AbortController();
this.setState({ loading: true, error: null });
fetch(`https://api.example.com/users/${userId}`, {
signal: this.abortController.signal,
})
.then(res => res.json())
.then(user => this.setState({ user, loading: false }))
.catch(err => {
if (err.name !== 'AbortError') {
this.setState({ error: err.message, loading: false });
}
});
};
render() {
const { user, loading, error } = this.state;
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>User: {user.name}</div>;
}
}
export default UserProfile;
Key points: The AbortController allows us to cancel the fetch in componentWillUnmount, preventing memory leaks. This pattern is the class-component equivalent of cleanup functions in useEffect.
Lifecycle Methods vs. Modern Hooks: Which Should You Use?
Modern React strongly prefers functional components with hooks over class components. The useEffect hook replaces all three lifecycle methods:
// Modern functional component with useEffect
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const abortController = new AbortController();
setLoading(true);
fetch(`https://api.example.com/users/${userId}`, {
signal: abortController.signal,
})
.then(res => res.json())
.then(user => setUser(user))
.finally(() => setLoading(false));
// Cleanup function (replaces componentWillUnmount)
return () => abortController.abort();
}, [userId]); // Dependency array (replaces componentDidUpdate logic)
if (loading) return <div>Loading...</div>;
return <div>User: {user?.name}</div>;
}
export default UserProfile;
New projects: Always use functional components and useEffect. Legacy code: Understanding class lifecycle methods is essential for maintaining older codebases (70% of React production code still uses class components as of 2026).
Frequently Asked Questions
What is the difference between componentDidMount and componentDidUpdate?
componentDidMount fires once after the first render; use it for one-time setup like API calls or subscriptions. componentDidUpdate fires after every re-render (except the initial one); use it to react to prop/state changes. Both receive the current this.props and this.state; only componentDidUpdate also receives previous values.
Do I need to use lifecycle methods in new React projects?
No. Modern React uses functional components with the useEffect hook, which is simpler and clearer. Lifecycle methods are legacy syntax. Use them only when maintaining old class-component codebases. All new features and best practices in React target functional components.
How do I prevent infinite loops in componentDidUpdate?
Always compare previous and current props/state before making API calls. Example: if (this.props.id !== prevProps.id) { this.fetchData(); }. Without this check, every update triggers a fetch, which triggers a re-render, which triggers another fetch. Use the dependency pattern from useEffect as a mental model: only act when a specific value changes.
What happens if I forget to clean up in componentWillUnmount?
Your timers, subscriptions, and pending requests continue running even after the component is gone. React will warn: "Can't perform a React state update on an unmounted component." This wastes memory and CPU and can cause subtle bugs. Always clean up: clear intervals, cancel requests, remove listeners, and unsubscribe.
Can I use async/await directly in componentDidMount?
No. componentDidMount cannot be async because it must return void, not a Promise. Instead, define an async function inside and call it: async function fetch() { ... } fetch(); Or use .then() chains as shown in the examples.