Module: Hooks Basics

Component Lifecycle Using Hooks

React JS: Hooks Basics -> Component Lifecycle Using Hooks

Introduction

Traditionally, managing component lifecycle events in React was done using class components and lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. With the introduction of Hooks in React 16.8, we can achieve the same functionality using functional components, making our code more concise and readable. This document explores how to replicate component lifecycle behavior using the useEffect hook.

The useEffect Hook

The useEffect hook is the primary way to perform side effects in functional components. Side effects are actions that interact with things outside the component, such as:

  • Fetching data
  • Setting up subscriptions (e.g., event listeners)
  • Manually changing the DOM
  • Logging

useEffect takes two arguments:

  1. A function: This function contains the side effect logic. It's executed after the component renders.
  2. An optional dependency array: This array controls when the effect function is re-executed.
useEffect(() => {
  // Your side effect logic here
  return () => {
    // Optional cleanup function
  };
}, [dependencies]);

Replicating Lifecycle Methods with useEffect

Let's see how we can map common lifecycle methods to useEffect usage:

1. componentDidMount (Equivalent)

To execute code only once after the component mounts (similar to componentDidMount), pass an empty dependency array ([]) to useEffect.

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

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // This code runs only once after the initial render
    console.log("Component mounted!");

    // Example: Fetching data
    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => setData(data));

    // No cleanup needed in this case
  }, []); // Empty dependency array

  return (
    <div>
      {data ? <p>Data: {JSON.stringify(data)}</p> : <p>Loading...</p>}
    </div>
  );
}

2. componentDidUpdate (Equivalent)

To execute code whenever the component updates (similar to componentDidUpdate), provide a dependency array with the values that, when changed, should trigger the effect.

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

function MyComponent({ value }) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // This code runs whenever 'value' or 'count' changes
    console.log("Component updated! Value:", value, "Count:", count);

    // Example:  Update document title based on value
    document.title = `Value: ${value}`;

  }, [value, count]); // Dependency array with 'value' and 'count'

  return (
    <div>
      <p>Value: {value}</p>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
    </div>
  );
}

Important Considerations for componentDidUpdate:

  • Avoid infinite loops: Be careful not to update state within the effect function in a way that causes it to re-run endlessly. For example, if you update a state variable that's also in the dependency array, you can create an infinite loop.
  • Compare previous and current values: If you need to know the previous value of a prop or state, you can use the useRef hook to store the previous value and compare it within the effect.

3. componentWillUnmount (Equivalent)

To execute code when the component unmounts (similar to componentWillUnmount), return a cleanup function from the effect function. This function will be called when the component is removed from the DOM.

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

function MyComponent() {
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() => {
    // This code runs after the component mounts
    console.log("Component mounted!");

    // Example: Setting up an event listener
    const handleResize = () => {
      console.log("Window resized!");
    };

    window.addEventListener('resize', handleResize);

    // Cleanup function
    return () => {
      console.log("Component unmounted!");
      // Example: Removing the event listener
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Empty dependency array

  return (
    <div>
      <p>Component is mounted: {isMounted ? 'Yes' : 'No'}</p>
    </div>
  );
}

Explanation:

  • The useEffect hook with an empty dependency array ensures the effect runs only once on mount.
  • The return () => { ... } part defines the cleanup function.
  • When the component unmounts, React will call this cleanup function, allowing you to remove event listeners, cancel subscriptions, or perform any other necessary cleanup tasks.

Combining Lifecycle Behaviors

You can combine these patterns to achieve more complex lifecycle behavior. For example, you can have an effect that runs on mount and unmount, and another effect that runs whenever specific props change.

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

function MyComponent({ data }) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("Component mounted or data changed!");

    // Perform actions on mount or when 'data' changes
    document.title = `Data: ${data}`;

    return () => {
      console.log("Component unmounted or data changed (cleanup)!");
      // Perform cleanup actions
    };
  }, [data]);

  useEffect(() => {
    console.log("Count changed!");
    // Perform actions when 'count' changes
  }, [count]);

  return (
    <div>
      <p>Data: {data}</p>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
    </div>
  );
}

Best Practices

  • Keep effects focused: Each useEffect hook should ideally handle a single, specific side effect. If you have multiple unrelated side effects, consider using multiple useEffect hooks.
  • Use descriptive dependency arrays: Clearly specify all the values that the effect depends on in the dependency array. This helps React optimize performance and prevents unexpected behavior.
  • Cleanup functions are crucial: Always provide a cleanup function when necessary to prevent memory leaks and other issues.
  • Consider using useCallback and useMemo: If you're passing functions or objects as dependencies to useEffect, use useCallback and useMemo to prevent unnecessary re-renders.

By understanding how to use useEffect effectively, you can replicate the functionality of class component lifecycle methods in functional components, leading to cleaner, more maintainable React code.