author Jeremiah Swank

React State: useEffect

In React its common to need to perform actions or “effects” at certain points in the component lifecycle. For example you may want to call an API whenever the component loads for the first time (mounts) or clear data from local storage whenever the component is removed (unmounts).

The useEffect Hook

The useEffect hook in React is a powerful tool for managing side effects in functional components. It allows developers to perform actions such as data fetching independent from the rendering of the component.

The useEffect hook takes a function as its first argument, which is executed after the component renders.

function FooBar() {
  useEffect(() => { 
    //Called after the component renders for the first time
    console.log("Component mounted");
    localStorage.setItem("load-time", new Date().toLocaleString());
  });

  return (
    <>
      <h1>Hello useEffect</h1>
    </>
  );
}

Optionally, you can return a funtion from the effect hook that will be called whenever the component is unmounted:

function FooBar() {
  useEffect(() => {
    //Called after the component renders for the first time
    console.log("Component mounted");
    localStorage.setItem("load-time", new Date().toLocaleString());

    return () => {
      // Called when the component is removed from the page
      console.log("Component unmounted");
      localStorage.removeItem("load-time");
    };
  });

  return (
    <>
      <h1>Hello useEffect</h1>
    </>
  );
}

This make it handy to perform cleanup tasks when the component is removed from the page.

Another common use case for useEffect is to perform an action whenever a state variable changes. The useEffect hook optionally takes an array of dependencies as its second argument. The effect function runs whenever the dependencies change, providing a way to synchronize the component’s state with external systems. For example:

function FooBar() {
  const [foo, setFoo] = useState(0);
  const [bar, setBar] = useState(0);
  const [baz, setBaz] = useState(0);

  useEffect(() => {
    //Called when foo and bar change but not baz
    localStorage.setItem("foo", foo);
    localStorage.setItem("bar", bar);
  }, [foo, bar]);

  return (
    <>
      <button onClick={() => setFoo(foo + 1)}> Increment Foo</button>
      <button onClick={() => setBar(bar + 1)}> Increment Bar</button>
      <button onClick={() => setBaz(baz + 1)}> Increment Baz</button>
    </>
  );
}

This useEffect will store the values of foo and bar in local storage whenever they change but nothing happens when baz changes.

Fetching Data

Lets explore a common use case for useEffect in React: fetching data from an API. Here we will integrate will an API for fetching pictures of Dogs. Here is the starter code:

import { useEffect } from "react";

function Dogs() {
  useEffect(() => {
    //Call API
  }, []);

  return (
    <>
      <img src="https://images.dog.ceo/breeds/bulldog-french/n02108915_12319.jpg" />
    </>
  );
}

We will use the fetch function to call the API. From there we need to store the data returned from the API in a state variable:

function Dogs() {
  const [dogs, setDogs] = useState([]);

  useEffect(() => {
    fetch("https://dog.ceo/api/breed/bulldog/french/images/random/3")
      .then((response) => response.json())
      .then((data) => setDogs(data.message));
  }, []);

  return (
    <>
      <img src="https://images.dog.ceo/breeds/bulldog-french/n02108915_12319.jpg" />
    </>
  );
}

Let brake down this code:

const [dogs, setDogs] = useState([]);

This is the state variable we will store the array of dog images in that is returned from the API. setDogs will be used to update the value after the API returns data. dogs is used to read the list of dog images.

  useEffect(() => {
    fetch("https://dog.ceo/api/breed/bulldog/french/images/random/5")
      .then((response) => response.json())
      .then((data) => setDogs(data.message));
  }, []);

This useEffect hook calls the dog API to get a list of random images. It is called AFTER the first render. fetch happens asynchronously so we need to use the then method to read the response that is returned and update our state variable. The [] as the second argument to useEffect tells it to run only whenever the component first renders.

Now that we have an array of images we can update our return statement to display the images:

 return (
    <>
      {dogs.map((url) => (
        <img key={url} src={url} />
      ))}
    </>
  );

Now whenever the component renders a list of random images will display on the page. Let take this a step further and add a button that will increase the number of dogs displayed on the page. Lets create a state variable to track the number of images to fetch:

function Dogs() {
  ...
  const [count, setCount] = useState(1);
  ...

  return (
    <>
      <button onClick={() => setCount(count + 1)}>More dogs!</button>
      {dogs.map((url) => (
        <img key={url} src={url} />
      ))}
    </>
  );
}

From here we can modify the useEffect hook to fetch more dogs when the value of count changes:

 useEffect(() => {
    fetch(`https://dog.ceo/api/breed/bulldog/french/images/random/${count}`)
      .then((response) => response.json())
      .then((data) => setDogs(data.message));
  }, [count]); // run effect everytime count changes

There are two imporant change here. The count variable is added to the API request in the fetch function. We also added the count variable to the dependency array. This is necessary to rerun the effect hook whenever count changes. Without this the effect only runs once when the component first mounts.

You’ll notice there is a flicker before the images are displayed. This is because the effect hook doesn’t run until after the first render. We can add a loading state to show the user that the images are loading. Lets crate a new state variable to track the loading state:

function Dogs() {
  ...
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetch(`https://dog.ceo/api/breed/bulldog/french/images/random/${count}`)
      .then((response) => response.json())
      .then((data) => setDogs(data.message))
      .finally(() => setLoading(false));
  }, [count]);

  if (loading) {
    return <p>Loading....</p>;
  }

  return (
    <>
      <button onClick={() => setCount(count + 1)}>More dogs!</button>
      {dogs.map((url) => (
        <img key={url} src={url} />
      ))}
    </>
  );
}

Here we set the loading to true at the start of the effect hook and set loading to false after the data is fetched. We conditionally render a loading message when loading is true. This gives a better user experience when loading data from the API is running slow.

Conclusion

Using the useEffect hook to fetch data from an API is a highly effective way to integrate asynchronous operations into functional components in React. By leveraging useEffect, developers can initiate data fetching operations when a component mounts or when specific dependencies change, ensuring that data is loaded and displayed appropriately. This approach simplifies the management of side effects and allows for clear, maintainable code that reacts to changes in data or state.

Additionally, useEffect facilitates proper handling of cleanup tasks, such as aborting ongoing requests, to prevent potential memory leaks. Overall, useEffect streamlines the process of fetching and managing data, enhancing the responsiveness and reliability of React applications.