The Lifecycle of a Component
Complete guide to React lifecycle methods in class components. Learn componentDidMount for data fetching, componentDidUpdate for prop changes, and componentWillUnmount for cleanup. Understand the mounting, updating, and unmounting phases with practical examples.
The Story So Far
I understood components. I grasped state. Things were going well with my little function components and useState.
Then I needed to fetch data when a component loaded.
“Just use useEffect,” someone said.
“What’s that?” I asked.
“Oh wait, you should probably understand lifecycle methods first. That’s what useEffect replaces.”
Record scratch. Freeze frame.
Welcome to Class Components
Before hooks (pre-React 16.8), if you wanted state or lifecycle methods, you needed class components:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Add One
</button>
</div>
);
}
}
Look at all that ceremony! constructor, super(props), this.state, this.setState. Coming from simple function components, this felt like putting on a suit just to go to the grocery store.
But class components gave us something function components didn’t have: lifecycle methods.
What Even Is a “Lifecycle”?
Every component goes through a lifecycle:
- Mounting - The component is created and inserted into the DOM
- Updating - The component re-renders due to state or prop changes
- Unmounting - The component is removed from the DOM
React gave us methods that run at specific points in this lifecycle. Think of them as hooks (lowercase h) into these moments.
The Big Three Lifecycle Methods
1. componentDidMount()
Runs once, right after the component first appears on screen.
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = { user: null, loading: true };
}
componentDidMount() {
// Perfect place to fetch data!
fetch(`/api/users/${this.props.userId}`)
.then(response => response.json())
.then(user => {
this.setState({ user, loading: false });
});
}
render() {
if (this.state.loading) return <div>Loading...</div>;
return <div>Hello, {this.state.user.name}!</div>;
}
}
Use cases:
- Fetching data from an API
- Setting up subscriptions
- Adding event listeners
- Starting timers
- Integrating with third-party libraries
This was the place to do anything that needed the component to exist in the DOM first.
2. componentDidUpdate(prevProps, prevState)
Runs after every re-render (but not the first one).
class UserProfile extends React.Component {
// ... constructor and componentDidMount ...
componentDidUpdate(prevProps, prevState) {
// Did the userId prop change?
if (prevProps.userId !== this.props.userId) {
// Fetch the new user!
this.setState({ loading: true });
fetch(`/api/users/${this.props.userId}`)
.then(response => response.json())
.then(user => {
this.setState({ user, loading: false });
});
}
}
render() {
// ...
}
}
The infinite loop trap:
componentDidUpdate() {
// 🚨 DANGER: This causes an infinite loop!
this.setState({ updated: true });
}
setState triggers a re-render, which triggers componentDidUpdate, which calls setState, which triggers a re-render… forever.
Always wrap setState in a condition:
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
// Now it only runs when userId actually changes
this.fetchUser();
}
}
3. componentWillUnmount()
Runs right before the component is removed from the DOM. Your cleanup crew.
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { seconds: 0 };
}
componentDidMount() {
this.interval = setInterval(() => {
this.setState(state => ({ seconds: state.seconds + 1 }));
}, 1000);
}
componentWillUnmount() {
// Clean up the interval!
clearInterval(this.interval);
}
render() {
return <div>Seconds: {this.state.seconds}</div>;
}
}
If you don’t clean up:
- Memory leaks
- Timers running after component is gone
- Event listeners piling up
- State updates on unmounted components (React warns about this)
The Full Lifecycle Diagram
Here’s what blew my mind - there were actually WAY more lifecycle methods:
MOUNTING:
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
UPDATING:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
UNMOUNTING:
componentWillUnmount()
ERROR HANDLING:
static getDerivedStateFromError()
componentDidCatch()
And there used to be even MORE that got deprecated:
componentWillMount(don’t use)componentWillReceiveProps(don’t use)componentWillUpdate(don’t use)
These were removed because they caused bugs with React’s new async rendering. If you see them in old code, that’s legacy stuff.
A Real-World Example: Chat Subscription
Here’s a pattern I used constantly:
class ChatRoom extends React.Component {
constructor(props) {
super(props);
this.state = { messages: [] };
}
componentDidMount() {
// Subscribe when component mounts
this.subscription = ChatAPI.subscribe(
this.props.roomId,
message => {
this.setState(state => ({
messages: [...state.messages, message]
}));
}
);
}
componentDidUpdate(prevProps) {
// Room changed? Resubscribe!
if (prevProps.roomId !== this.props.roomId) {
// Unsubscribe from old room
this.subscription.unsubscribe();
// Subscribe to new room
this.subscription = ChatAPI.subscribe(
this.props.roomId,
message => {
this.setState(state => ({
messages: [...state.messages, message]
}));
}
);
// Clear old messages
this.setState({ messages: [] });
}
}
componentWillUnmount() {
// Clean up subscription
this.subscription.unsubscribe();
}
render() {
return (
<div>
{this.state.messages.map(msg => (
<p key={msg.id}>{msg.text}</p>
))}
</div>
);
}
}
Notice the problem? The subscription logic is scattered across three methods. Mount, update, and unmount all need to coordinate. It’s easy to forget one piece or introduce bugs.
The Pain Points
After months with class components, I had a list of grievances:
1. The this Keyword Nightmare
class Button extends React.Component {
handleClick() {
console.log(this.props.label); // 💥 TypeError: Cannot read property 'props' of undefined
}
render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}
The fix? Binding in the constructor:
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
Or using arrow functions:
handleClick = () => {
console.log(this.props.label); // Works!
}
I can’t tell you how many hours I lost to this being undefined.
2. Related Logic is Split Apart
Want to set up and tear down a subscription? That’s componentDidMount AND componentWillUnmount. Want to also handle prop changes? Add componentDidUpdate. Your “one feature” is now spread across three methods.
3. Code Reuse is Awkward
Want to share stateful logic between components? Your options were:
- Higher-Order Components (HOCs) - Functions that wrap components
- Render Props - Components that take functions as children
Both worked but created “wrapper hell” in the component tree:
<ThemeProvider>
<AuthProvider>
<DataFetcher>
<LocalizationProvider>
<ActualComponent /> {/* Buried under 4 layers */}
</LocalizationProvider>
</DataFetcher>
</AuthProvider>
</ThemeProvider>
4. Classes Are Just… Harder
- You have to understand
this - You have to understand binding
- You have to understand the constructor pattern
- Minifiers have a harder time with classes
- Hot reloading can be flaky
The Light at the End of the Tunnel
By late 2018, I was proficient with class components but frustrated by them. Then, in February 2019, React 16.8 dropped.
Hooks.
Suddenly, everything I was doing with lifecycle methods could be done differently. Cleaner. More intuitive.
But that’s a story for the next post.
P.S. — If you’re learning React today, you might wonder why I’m even teaching class components. Fair question. But understanding lifecycles helps you understand what hooks are actually doing under the hood. Plus, you’ll encounter class components in legacy codebases. Trust me, this knowledge pays off.
Saurav Sitaula
Software Architect • Nepal