Published on

4 - Lifecycle and Effects in React: Mastering useEffect and Lifecycle Methods in Class Components

Authors
  • avatar
    Name
    Jonas de Oliveira
    Twitter

In the world of React development, understanding and mastering component lifecycles and side effects is essential for building robust, efficient, and maintainable applications. Whether you're using modern hooks or exploring traditional lifecycle methods in class components, knowing these approaches broadens your ability to create scalable, high-quality solutions—a valuable asset to attract recruiters' attention.

useEffect for Handling Side Effects

With the introduction of hooks, React simplified the management of side effects using useEffect. This hook is essential for handling tasks such as API calls, event subscriptions, timer manipulations, and other operations that need to occur outside the main rendering flow.

Key Features of useEffect

  • Replacement for Lifecycle Methods: useEffect can replicate behaviors that were previously implemented with methods like componentDidMount, componentDidUpdate, and componentWillUnmount.
  • Dependencies: The dependency array controls when the effect should execute. If left empty, the effect runs only once, simulating the behavior of componentDidMount.
  • Effect Cleanup: By returning a function from useEffect, you can clean up effects to prevent memory leaks and unintended behavior.

Practical Example with useEffect

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

// Define a functional component called DataFetcher
const DataFetcher: React.FC = () => {
  // Initialize the "data" state as an array of strings to hold the API data
  const [data, setData] = useState<string[]>([]);

  // useEffect hook to perform side effects (API call) when the component mounts
  useEffect(() => {
    // Simulate an API call to fetch data from the given URL
    fetch('https://api.example.com/data')
      .then(response => response.json()) // Convert the response to JSON format
      .then(data => setData(data)) // Update the "data" state with the fetched data
      .catch(error => console.error('Error fetching data:', error)); // Log any errors to the console
  }, []); // Empty dependency array ensures this effect runs only once after the component mounts

  return (
    <div>
      <h2>API Data</h2>
      <ul>
        {/* Iterate over the "data" array and render each item as a list element */}
        {data.map((item, index) => (
          <li key={index}>{item}</li> // Use the index as key (ensure keys are unique in real applications)
        ))}
      </ul>
    </div>
  );
};

export default DataFetcher;

In this example, useEffect is used to simulate an API call right after the component mounts, demonstrating how to handle side effects in a simple and safe way using TypeScript.

Class Components: Exploring the Lifecycle

Before hooks became popular, class components were the primary way to manage a component’s lifecycle in React. These components use specific methods that allow you to execute code at strategic moments during a component's existence.

Essential Lifecycle Methods

  • componentDidMount: Executes immediately after the component is mounted. Ideal for initiating asynchronous operations such as API calls.
  • componentDidUpdate: Invoked whenever the component updates. Useful for reacting to changes in props or state.
  • componentWillUnmount: Called just before the component is unmounted, allowing you to clean up subscriptions, timers, or other resources.

Practical Example with Class Components

import React, { Component } from 'react';

// Define an interface for the component state, which includes the current time
interface ClockState {
  time: Date;
}

// Create a class component "Clock" that uses lifecycle methods to manage its state
class Clock extends Component<{}, ClockState> {
  // Optional property to store the timer ID for later cleanup
  timerId?: number;

  // Constructor to initialize the component state with the current time
  constructor(props: {}) {
    super(props);
    this.state = { time: new Date() };
  }

  // componentDidMount is called once the component is inserted into the DOM
  componentDidMount() {
    // Start a timer that updates the state with the current time every second
    this.timerId = window.setInterval(() => {
      this.setState({ time: new Date() });
    }, 1000);
  }

  // componentDidUpdate is called after the component updates
  componentDidUpdate(prevProps: {}, prevState: ClockState) {
    // Log the updated time when the state changes
    if (prevState.time !== this.state.time) {
      console.log('Time updated:', this.state.time.toLocaleTimeString());
    }
  }

  // componentWillUnmount is called right before the component is removed from the DOM
  componentWillUnmount() {
    // Clear the timer to prevent memory leaks
    if (this.timerId) {
      clearInterval(this.timerId);
    }
  }

  // Render method to display the current time
  render() {
    return <h2>Current Time: {this.state.time.toLocaleTimeString()}</h2>;
  }
}

export default Clock;

This example illustrates how lifecycle methods in class components can be used to manage asynchronous tasks and ensure resource cleanup, keeping the application stable and responsive.

Final Thoughts

Mastering lifecycle management and side effects in React is a crucial skill for any developer aiming to build modern, scalable applications. Whether you're using useEffect in functional components or exploring traditional lifecycle methods in class components, this knowledge demonstrates your commitment to best practices and your ability to create robust solutions.