react

The Definitive Guide to useEffect

author

Luis Paredes

published

Jun 9, 2023

When working with React, it's essential to understand how to manage side effects within your components. This is where the useEffect hook comes into play.

What are side effects?

In frontend applications, common side effects refer to operations or interactions that occur outside the scope of the application's core logic and can affect the application's state, user interface, or the environment it operates in.

These side effects typically involve tasks such as making network requests, manipulating the DOM, subscribing to events, accessing browser APIs, or interacting with external services. Side effects are necessary to handle asynchronous operations, integrate with external systems, and ensure proper interaction with the user interface and the browser environment.

Managing these side effects effectively is crucial for maintaining a robust and responsive frontend application.

What is useEffect and when do you need to use it?

useEffect is a built-in hook in React that allows you to perform side effects in functional components. It enables you to handle tasks like data fetching, subscribing to events, or modifying the DOM.

In the next sections, we'll see the typical useEffect patterns as well as examples of how to use it for the most common use cases.

useEffect patterns

The useEffect function takes two parameters: a callback function and an optional array of dependencies. The callback function represents the side effect you want to perform, and when and how often it will be called will depend on whether or not you pass the second parameter and the contents of that parameter when you pass it.

Using useEffect without dependencies

If you intend to call the side effect callback after every render of the component, you can invoke the hook using only the first argument:

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // Side effect code goes here
    // This code will run after every render
  });

  return (
    // Component JSX
  );
}

Using useEffect with an empty dependency array

In some cases, you may want to execute the side effect callback only once when the component mounts. To achieve this, you can provide an empty dependency array as the second argument to useEffect:

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // Side effect code goes here
    // This code will run only once when the component mounts
  }, []);

  return (
    // Component JSX
  );
}

By passing an empty array as the dependency list, you indicate that the effect has no dependencies and should only execute once during the component's initial render. This is particularly useful when performing tasks like:

  • initializing external libraries,
  • setting up event listeners, or
  • subscribing to data sources.

It's important to note that the effect will not be triggered again even if the component re-renders, making it an ideal choice for scenarios where you want to perform setup or initialization operations that should not be repeated unnecessarily.

Using useEffect with dependencies

By providing an array of dependencies as the second parameter to useEffect, you can control when the effect should run. The effect will only execute if one or more of the dependencies have changed since the last render.

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

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

  useEffect(() => {
    // Side effect code goes here
    // This code will run when `data` changes
  }, [data]);

  return (
    // Component JSX
  );
}

Using useEffect with a cleanup function

In some cases, your side effect might need to perform cleanup before the component unmounts or before the effect is run again. Returning a cleanup function is useful for tasks like:

  • canceling subscriptions,
  • removing event listeners, or
  • clearing timers.

To clean up an effect, you can return a cleanup function from the effect's callback function. The cleanup function will be executed before the component is removed from the DOM or before the effect is run again.

import React, { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // Side effect code goes here

    return () => {
      // Cleanup code goes here
      // This code will run before the component unmounts or before the effect is run again
    };
  });

  return (
    // Component JSX
  );
}

Common use cases

useEffect is a versatile hook that can be used in various scenarios. In this section, we'll explore some common use cases and demonstrate how useEffect can be applied effectively.

Fetching data with useEffect

One common use case is fetching data from an API. With useEffect, you can trigger the data fetching operation when the component renders for the first time or when certain dependencies change.

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

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

  useEffect(() => {
    // Fetch data from API
    fetchData().then((response) => setData(response));
  }, []);

  return (
    // Render the data
  );
}

Subscribing to events with useEffect

Another use case is subscribing to events, such as window resize or keyboard events. With useEffect, you can add event listeners and remove them when the component unmounts.

import React, { useEffect } from 'react';

function EventComponent() {
  useEffect(() => {
    const handleResize = () => {
      // Handle resize event
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    // Component JSX
  );
}

Updating UI based on changes in props

Another powerful use case for useEffect is updating the user interface based on changes in dependencies. This allows you to dynamically modify the UI based on certain conditions or external data.

Let's explore an example of how useEffect can be used to update the background color of calendar days based on events assigned to those days.

import { useEffect, useState } from "react";

interface Day {
  date: string;
  backgroundColor: string;
}

interface Event {
  date: string;
  name: string;
}

interface CalendarProps {
  events: Event[];
  days: Day[];
}

function Calendar({ events, days }: CalendarProps) {
  const [calendarData, setCalendarData] = useState<Day[]>(days);

  useEffect(() => {
    // Update the background color of days with events
    const updatedCalendarData = days.map((day: Day) => {
      if (events.map(({ date }: Event) => date).includes(day.date)) {
        return { ...day, backgroundColor: "yellow" };
      } else {
        return day;
      }
    });

    setCalendarData(updatedCalendarData);
  }, [events.toString(), days.toString()]);

  return (
    <div>
      <h1>Calendar</h1>
      <ul>
        {calendarData.map((day: Day) => (
          <li key={day.date} style={{ backgroundColor: day.backgroundColor }}>
            {day.date}
          </li>
        ))}
      </ul>
    </div>
  );
}

In this example, we have a Calendar component that takes in two props: events and days. The events prop is an array of Event objects, which contain information about each event, including the date and name. The days prop represents an array of Day objects, which contain the date and the background color of each day in the calendar.

Inside the Calendar component, we initialize the state variable calendarData using the useState hook. It holds an array of Day objects, initially set to the value of the days prop.

We then use the useEffect hook to update the calendarData whenever the events or days dependencies change. This hook allows us to perform side effects in response to changes in props or state. In our case, we want to update the background color of the calendar days based on the events assigned to those days.

The effect function performs the following steps:

  1. It maps through the days array and checks if each day's date is included in the events array. It does this by using the map function on events to extract the dates, and then checks if the current day's date is included in that array.
  2. If the day's date is found in the events array, it creates a new Day object with the same date and a background color of "yellow". Otherwise, it returns the original Day object.
  3. The updated array of Day objects is then set using the setCalendarData function, which triggers a re-render of the component.

Finally, the Calendar component renders the calendar UI based on the calendarData state. It maps through the calendarData array and renders a list item for each day, setting the background color dynamically based on the backgroundColor property of each Day object.

This example demonstrates how useEffect can be used to update the UI based on changes in the events prop. It allows us to dynamically modify the background color of the calendar days, providing visual feedback when events are assigned to specific dates.

Best practices

To ensure optimal usage of useEffect and avoid common pitfalls, consider the following best practices and tips:

  • Be mindful of the dependencies array and include only the necessary dependencies
  • Use multiple useEffect calls to separate unrelated effects and improve code readability
  • Optimize performance by optimizing the side effect's code and minimizing unnecessary re-renders
  • Be cautious with infinite loops by ensuring that the dependencies provided in the array are correctly updated
  • Do NOT use arrays, objects or other types that inherit from them as dependencies, the reason being that even if their contents don't change, their reference will change between renders and as a result the effect callback will be called on every render regardless of whether or not it was actually needed

Conclusion

In this comprehensive guide, we've covered the essentials of how useEffect can be used to handle side effects in different scenarios.

Hopefully by now, you're able to use the hook confidently in your apps and leverage its utility to write better React apps.