
Even though modern React codebases have shifted toward Hooks, understanding class components remains crucial for any serious React developer. Here's why: if you truly grasp class component lifecycle methods, you'll automatically understand when React renders, when side effects run, how updates are controlled, and why hooks behave the way they do.
This isn't just another definition list. We'll explore each lifecycle method with practical context, real-world use cases, and direct comparisons to their functional equivalents.
The Three Phases of React Component Lifecycle
Every React class component goes through three distinct phases:
- Mounting – When the component is created and inserted into the DOM
- Updating – When props or state change, triggering a re-render
- Unmounting – When the component is removed from the DOM
There's also a fourth phase for Error Handling, which we'll cover at the end.
Phase 1: Mounting (Birth of a Component)
Mounting happens exactly once—when your component first appears on screen.
Execution order:
constructor → getDerivedStateFromProps → render → componentDidMount
1. constructor(props)
This is where everything begins. The constructor creates your component instance and sets up initial state.
constructor(props) {
super(props);
this.state = {
count: props.initialCount || 0
};
}
What you should do here:
- Initialize state
- Bind event handlers (if not using arrow functions)
What you shouldn't do:
- Make API calls
- Set up subscriptions
- Cause side effects
Functional equivalent:
const [count, setCount] = useState(initialCount || 0);
2. static getDerivedStateFromProps(props, state)
This static method runs before every render and lets you sync state with incoming props.
static getDerivedStateFromProps(props, state) {
if (props.resetCount && state.count !== 0) {
return { count: 0 };
}
return null;
}
Return values:
-
object→ merges into state -
null→ no state update
Important: This is a static method, so you don't have access to this. It's also considered an anti-pattern in most cases—React team recommends avoiding it unless absolutely necessary.
Functional equivalent:
There isn't a direct one, but you'd typically handle this with useEffect:
useEffect(() => {
if (resetCount) {
setCount(0);
}
}, [resetCount]);
3. render()
The heart of every React component. This method describes what your UI should look like.
render() {
return <div>{this.state.count}</div>;
}
Rules:
- Must be pure (no side effects)
- No
setStatecalls - No direct DOM manipulation
- Must return JSX or
null
Functional equivalent:
The entire function body is essentially the render method:
return <div>{count}</div>;
4. componentDidMount()
This runs immediately after your component is inserted into the DOM. It's the perfect place for side effects.
componentDidMount() {
fetch('/api/data')
.then(res => res.json())
.then(data => this.setState({ data }));
this.timer = setInterval(() => {
this.setState({ time: new Date() });
}, 1000);
}
Common use cases:
- API calls
- Setting up subscriptions
- Starting timers
- Direct DOM manipulation (if needed)
Functional equivalent:
useEffect(() => {
// Your side effects here
}, []); // Empty dependency array = runs once on mount
Phase 2: Updating (Component Re-renders)
Updates happen when state or props change.
Execution order:
getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate
5. shouldComponentUpdate(nextProps, nextState)
This is your performance optimization checkpoint. It lets you prevent unnecessary re-renders.
shouldComponentUpdate(nextProps, nextState) {
return nextProps.id !== this.props.id ||
nextState.count !== this.state.count;
}
Return values:
-
true→ proceed with render -
false→ skip render (and all subsequent lifecycle methods)
Functional equivalent:
const MemoizedComponent = React.memo(Component, (prevProps, nextProps) => {
return prevProps.id === nextProps.id; // true = skip render
});
6. getSnapshotBeforeUpdate(prevProps, prevState)
This rarely-used method captures information from the DOM right before React applies updates.
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
The returned value gets passed to componentDidUpdate as the third argument.
Functional equivalent:
useLayoutEffect(() => {
// Runs synchronously after DOM mutations
}, [dependencies]);
7. componentDidUpdate(prevProps, prevState, snapshot)
Runs after the component updates in the DOM. Perfect for reacting to prop or state changes.
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUserData(this.props.userId);
}
}
Critical rule: Always compare previous values before calling setState, or you'll create an infinite loop.
Functional equivalent:
useEffect(() => {
fetchUserData(userId);
}, [userId]); // Runs when userId changes
Phase 3: Unmounting (Cleanup)
8. componentWillUnmount()
This is your cleanup phase. It runs right before the component is destroyed.
componentWillUnmount() {
clearInterval(this.timer);
this.subscription.unsubscribe();
document.removeEventListener('scroll', this.handleScroll);
}
Common cleanup tasks:
- Clear timers
- Cancel network requests
- Remove event listeners
- Unsubscribe from services
Functional equivalent:
useEffect(() => {
const timer = setInterval(() => {}, 1000);
return () => {
clearInterval(timer); // Cleanup function
};
}, []);
Error Handling Phase
9. static getDerivedStateFromError(error)
Updates state when a child component throws an error, allowing you to render a fallback UI.
static getDerivedStateFromError(error) {
return { hasError: true };
}
10. componentDidCatch(error, info)
Logs error details for monitoring and debugging.
componentDidCatch(error, info) {
console.error('Error caught:', error);
console.error('Component stack:', info.componentStack);
// Send to error reporting service
}
Note: There's no direct hook equivalent for error boundaries yet. You still need class components for this.
Real-World Example: User Profile with Full Lifecycle
Let's build a component that demonstrates multiple lifecycle methods working together:
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null,
loading: true,
error: null
};
}
componentDidMount() {
this.fetchUser(this.props.userId);
}
componentDidUpdate(prevProps) {
// Fetch new user data when userId prop changes
if (prevProps.userId !== this.props.userId) {
this.setState({ loading: true });
this.fetchUser(this.props.userId);
}
}
componentWillUnmount() {
// Cancel any pending requests
if (this.controller) {
this.controller.abort();
}
}
fetchUser(userId) {
this.controller = new AbortController();
fetch(`/api/users/${userId}`, {
signal: this.controller.signal
})
.then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
})
.then(user => this.setState({
user,
loading: false,
error: null
}))
.catch(error => {
if (error.name !== 'AbortError') {
this.setState({
error: error.message,
loading: false
});
}
});
}
render() {
const { loading, error, user } = this.state;
if (loading) return <div className="spinner">Loading...</div>;
if (error) return <div className="error">Error: {error}</div>;
if (!user) return null;
return (
<div className="user-profile">
<img src={user.avatar} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
}
What this demonstrates:
-
constructorsets initial state -
componentDidMountfetches data on first render -
componentDidUpdatehandles prop changes intelligently -
componentWillUnmountprevents memory leaks -
renderstays pure and simple
Key Takeaways
Understanding class component lifecycle methods gives you:
✅ Better debugging skills – You'll know exactly when and why components re-render
✅ Deeper React mental model – Hooks aren't magic, they're lifecycle methods in disguise
✅ Legacy codebase confidence – Many production apps still use class components
✅ Performance optimization knowledge – You'll understand when to use memo, useMemo, and useCallback
Hooks didn't replace lifecycle concepts—they just changed the API. The underlying principles remain the same.
Whether you're maintaining legacy code or just want to truly understand React, mastering lifecycle methods is time well spent.
Have you worked with class components in production? What lifecycle method patterns have you found most useful? Drop your experiences in the comments!
Top comments (0)