Mastering React Component Life Cycles: From Classes to Hooks

Savan Chhayani
TechVerito
Published in
5 min readJun 5, 2024
Photo by Abed Ismail on Unsplash

React is a powerful Javascript library for building user interfaces. One of the key concepts in React is the component lifecycle. Understanding this lifecycle is essential for building robust and efficient components. In this blog post, we’ll explore the lifecycle of React components, both in class-based and functional components.

Class Components: The Traditional way

Class components in React have specific methods that are invoked at different stages of a component’s life. Here’s a detailed look at these methods:

Mounting Phase

Mounting is the phase when a component is being inserted into the DOM.

  • constructor(): This method is called before the component is mounted. It’s typically used for initializing state and binding event handlers.
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = { seconds: 0 }
}
}
  • componentDidMount(): This method is invoked immediately after a component is mounted. It’s a good place to make API calls or interact with the DOM.
class Timer extends React.Component {
componentDidMount() {
this.interval = setInterval(() => {
this.setState(preveState => ({
seconds: prevState.seconds + 1,
}))
}, 1000)
}
}

Updating Phase

Updating happens when a component’s state or props change.

  • shouldComponentUpdate(): This method is called before rendering when new props or state are received. It returns a boolean value to decide whether the component should be updated. Useful for performance optimization.
class Timer extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return nextState.seconds % 2 === 0; // Update component only on even seconds.
}
}
  • componentDidUpdate(): This method is called immediately after updating occurs. It’s useful for performing DOM operations or making further API calls based on updated props or state.
class Timer extends React.Component {
componentDidUpdate(preProps, prevState) {
if (prevState.seconds !== this.state.seconds) {
console.log(`Component is updated to ${this.state.seconds} seconds`);
}
}
}

Unmounting Phase

It is the phase when a component is being removed from the DOM.

  • componentWillUnmount(): This method is called right before a component is unmounted and destroyed. It’s used for clean-up activities such as canceling network requests or removing event listeners.
class Timer extends React.Component {
componentWillUnmount() {
clearInterval(this.interval);
}
}
Photo by 五玄土 ORIENTO on Unsplash

Putting It All Together: A Comprehensive Timer Component

import React from 'react';

class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {
seconds: 0
};
this.interval = null; // Initialize interval variable
}

componentDidMount() {
// Start the timer when the component is mounted
this.interval = setInterval(() => {
this.setState(prevState => ({
seconds: prevState.seconds + 1
}));
}, 1000);
}

componentDidUpdate(prevProps, prevState) {
// This method is called after every update
if (prevState.seconds !== this.state.seconds) {
console.log(`Timer updated: ${this.state.seconds} seconds`);
}
}

componentWillUnmount() {
// Clear the interval when the component is unmounted
clearInterval(this.interval);
console.log('Timer stopped');
}

render() {
return (
<div>
<h1>Timer: {this.state.seconds} seconds</h1>
</div>
);
}
}

export default Timer;

Functional Components: The Modern Approach

With the advent of hooks in React 16.8, functional components gained the ability to manage state and side effects. Here’s how you can achieve similar life cycle management in functional components:

Mounting Phase

  • useEffect(): Runs after the first render. This makes it perfect for initialization tasks.
import React, { useState, useEffect } from 'react';

function Timer() {
const [seconds, setSeconds] = useState(0);

useEffect(() => {
console.log('Component did mount!');
}, []); // The empty array means this effect runs only once.

return (
<div>
<h1>Timer: {seconds} seconds</h1>
</div>
)
}

In this example, the useEffect hook logs a message when the component mounts. The empty dependency array ([]) ensures that this effect runs only once, mimicking the componentDidMount lifecycle method in class components.

Updating Phase

useEffect() with Dependencies: The useEffect hook can also handle updates by specifying dependencies. The effect runs whenever the specified dependencies change.

import React, { useState, useEffect } from "react";

function Timer() {
const [seconds, setSeconds] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setSeconds((prevSeconds) => prevSeconds + 1);
}, 1000);

return () => clearInterval(interval); // Cleanup function
}, []); // Run only once on mount

useEffect(() => {
console.log(`Component updated to ${seconds} seconds`);
}, [seconds]); // Run whenever 'seconds' changes

return (
<div>
<h1>Timer: {seconds} seconds</h1>
</div>
);
}

Here, the first useEffect sets up an interval timer to increment the seconds state every second. The second useEffect logs a message whenever the seconds state changes, demonstrating how to handle component updates.

Unmounting Phase

useEffect() Cleanup Function: The useEffect hook can return a cleanup function that runs when the component unmounts, allowing you to clean up resources such as subscriptions or timers.

import React, { useState, useEffect } from 'react';

function Timer() {
const [seconds, setSeconds] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);

return () => {
clearInterval(interval);
console.log('Component will unmount!');
};
}, []); // Empty array ensures this runs only once on mount and cleanup on unmount

return (
<div>
<h1>Timer: {seconds} seconds</h1>
</div>
);
}

export default Timer;

In this example, the cleanup function within the useEffect hook clears the interval timer and logs a message when the component unmounts, mimicking the componentWillUnmount lifecycle method in class components.

Putting It All Together: A Comprehensive Timer Component

Now let’s combine all these aspects into a more comprehensive timer component that includes start, pause, and reset functionality.

import React, { useState, useEffect } from 'react';

function Timer() {
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);

useEffect(() => {
let interval;
if (isActive) {
interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
}
return () => clearInterval(interval); // Cleanup on unmount or when isActive changes
}, [isActive]);

const toggle = () => setIsActive(!isActive);
const reset = () => {
setSeconds(0);
setIsActive(false);
};

return (
<div>
<h1>Timer: {seconds} seconds</h1>
<button onClick={toggle}>{isActive ? 'Pause' : 'Start'}</button>
<button onClick={reset}>Reset</button>
</div>
);
}

export default Timer;

Explanation:

  • Mounting Phase: The timer starts when the component mounts if isActive is true.
  • Updating Phase: The timer updates every second when isActive changes.
  • Unmounting Phase: The interval is cleared when the component unmounts or when isActive changes.

Conclusion:

React’s component lifecycle is an essential concept that every React developer should understand. Whether you prefer class components with their explicit lifecycle methods or functional components with hooks, mastering these lifecycles will enhance your ability to build efficient and responsive applications.

By understanding and leveraging these lifecycle methods, you can ensure your components are mounted, updated, and unmounted correctly, leading to better performance and maintainability in your React applications.

Happy Coding ⌨️

Photo by Dell on Unsplash

--

--

Savan Chhayani
TechVerito

Senior Consultant At Techverito React | JavaScript | TypeScript | Kotlin | Spring Boot | Docker