Why is the useEffect hook used in fetching data in React?
Tags: react, frontend, webdev, data
Published: 2024-06-11 15:50:29

To make this as simple as possible, I'll _avoid talking about Next.js_. So you want to _fetch data from a server_? What's the first thing that comes to your mind? Create a _function to handle the request_. **That makes sense. What could go wrong here?**

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

function FetchDataComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // Bad practice: Fetching data directly within the component rendering logic
  const fetchData = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      const data = await response.json();
      setData(data);
    } catch (error) {
      setError(error.message);
    } finally {
      setLoading(false);
    }
  };

  // Simulating a bad approach: Directly calling fetchData within the component rendering logic
  fetchData();

  return (
    <div className="fetch-data-container">
      <h1>Fetch Data Example</h1>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      {data && (
        <div>
          <h2>{data.title}</h2>
          <p>{data.body}</p>
        </div>
      )}
    </div>
  );
}

export default FetchDataComponent;

 
```

##Back to React basics
I think this is a _straightforward answer_. **Infinite loops!** Because when the state of a component changes, the component and its children get re-rendered. Using a `useState` hook to modify the **state without any constraints causes an infinite loop** to occur. 

### Effects! Effects! Effects!
Effects should happen **after the render phase** of your component. Effects usually happen when the **state** of a component changes, the **props** of a component change, **DOM manipulation**, **data fetching**, and even **user interaction** causes effects to happen which causes a component to re-render. By using a `useEffect` hook, you make sure **the effect happens after the initial render** and also re-renders depending on what is placed in the dependency array (the second parameter) of the `useEffect` hook. This means if the **dependency array is empty, it only runs once.**

##Add a `useEffect` hook, right?
Let's say I add a `useEffect` hook, we get **controlled effects**, and we can make sure the effects only occur once **after the initial render**. Plus, sometimes we only want to **fetch data once**.

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

function FetchDataComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setData(data);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []); // Empty dependency array means this effect runs once after the initial render

  return (
    <div className="fetch-data-container">
      <h1>Fetch Data Example</h1>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      {data && (
        <div>
          <h2>{data.title}</h2>
          <p>{data.body}</p>
        </div>
      )}
    </div>
  );
}

export default FetchDataComponent;

```

##But is something missing?
_It can't be that simple_. Can it? What happens when you want to change the data you fetched **because of user interaction**? A lot of the time, **data isn't static in a React application**. Let's assume we want to create a **product listing app** that **fetches different items** based on **different categories**.

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

const categories = ['Electronics', 'Clothing', 'Books'];

function ProductListComponent() {
  const [selectedCategory, setSelectedCategory] = useState(null);
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!selectedCategory) return;

    const fetchProducts = async () => {
      setLoading(true);
      setError(null);

      try {
        // Simulate fetching data from an API based on the selected category
        const response = await fetch(`https://api.example.com/products?category=${selectedCategory}`);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        setProducts(data);
      } catch (error) {
        setError(error.message);
      } finally {
        setLoading(false);
      }
    };

    fetchProducts();
  }, [selectedCategory]);

  return (
    <div className="product-list-container">
      <h1>Product List</h1>
      <div>
        {categories.map(category => (
          <button key={category} onClick={() => setSelectedCategory(category)}>
            {category}
          </button>
        ))}
      </div>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      {products.length > 0 && (
        <ul>
          {products.map(product => (
            <li key={product.id}>
              <h2>{product.name}</h2>
              <p>Price: ${product.price}</p>
              <p>{product.description}</p>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default ProductListComponent;

```

## `useEffect` can't provide everything
`useEffect` shines when you're trying to control effects in your app, but for data fetching, the concept doesn't reach far.

### Loading & Error States
You must have noticed that I've been **manually setting loading and error states**. Which creates **extra lines of code and complexity**.

---


### Race Conditions
A race condition occurs when **multiple asynchronous tasks are trying to update the same value at the same time.** Since there are three **different buttons to fetch different data** for each, a request **to fetch one category might be slower** than the other. Making **your state unpredictable and inconsistent**.

---

####Example of a race condition
Let's assume a user **clicks on the `electronics` button, but quickly changes it to the `clothing` button**. The user clicked the `clothing` button last. **The component gets re-rendered, but the previous fetch is still happening simultaneously with the new one**. So `clothing` data should show up? **If the request for fetching `electronics` data was slower than `clothing`**, **`electronics` should finish last**. And that's the data that'll show up. This makes the state inconsistent, **like the user didn't click the `clothing` button last**.

---

 

####How to fix a race condition
You can fix this issue by using **a cleanup function in your `useEffect` and a boolean flag**. This is a solution from the React official docs. The way this works is through closures. When the component is re-rendered, the **cleanup function updates the state of the previous effect** using the `ignore` boolean flag. You can also add `AbortController` and `AbortSignal` to prevent unnecessary network traffic from the user.

```
useEffect(() => {
  if (!selectedCategory) return;

  let ignore = true; // Flag to track component mount status

  const fetchProducts = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(`https://api.example.com/products?category=${selectedCategory}`);
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      const data = await response.json();
      if (ignore) {
        setProducts(data);
      }
    } catch (error) {
      if (ignore) {
        setError(error.message);
      }
    } finally {
      if (ignore) {
        setLoading(false);
      }
    }
  };

  fetchProducts();

  // Cleanup function to set the flag to false
  return () => {
    ignore = false;
  };
}, [selectedCategory]);


```

###Lack of caching
You have to **manually cache your data for every request** when using the `useEffect` hook to fetch data. Caching helps with a good user experience. Caching is important because it **makes your app seem fast**, let's assume a user clicks to another page and then goes back, without caching the user will see a loader (if you have one) again because the **data was re-fetched again**.

#### Fix for caching
We can use the browser's local storage to save the fetched data and use it when needed.

```
useEffect(() => {
    if (!selectedCategory) return;

    // Check if data is available in local storage
    const cachedData = localStorage.getItem(`products_${selectedCategory}`);
    if (cachedData) {
      setProducts(JSON.parse(cachedData));
      return;
    }

    let ignore = false; // Flag to track component unmount status

    const fetchProducts = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(`https://api.example.com/products?category=${selectedCategory}`);
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        const data = await response.json();
        if (!ignore) {
          setProducts(data);
          localStorage.setItem(`products_${selectedCategory}`, JSON.stringify(data)); // Save data to local storage
        }
      } catch (error) {
        if (!ignore) {
          setError(error.message);
        }
      } finally {
        if (!ignore) {
          setLoading(false);
        }
      }
    };

    fetchProducts();

    // Cleanup function to set the flag to true
    return () => {
      ignore = true;
    };
  }, [selectedCategory]);
```

###Refetches
In some apps, you might build, the **data you fetch has to change constantly**. To **avoid it from getting _stale_**, you need to perform background updates and re-fetches. The number of available products could change or the number of reviews could also change.

#### Let's add a quick fix
We can use an interval to re-fetch the data and then clear the interval in the cleanup function.
```
// UseEffect to fetch data when selectedCategory changes
  useEffect(() => {
    if (!selectedCategory) return;

    // Check if data is available in local storage
    const cachedData = localStorage.getItem(`products_${selectedCategory}`);
    if (cachedData) {
      setProducts(JSON.parse(cachedData));
    } else {
      fetchProducts(selectedCategory);
    }

    let ignore = false; // Flag to track component unmount status

    // Interval for refetching data periodically
    const intervalId = setInterval(() => {
      fetchProducts(selectedCategory, ignore);
    }, 60000); // Refetch every 60 seconds

    // Cleanup function to clear the interval and set the flag to true
    return () => {
      clearInterval(intervalId);
      ignore = true;
    };
  }, [selectedCategory]);
```

##Let's bring in React Query
From all these problems we weren't just talking about data fetching we were also talking about **managing the state in the app properly**. **React Query isn't just for data fetching**, it's an **async state manager**. It fixes all the bugs from the `useEffect` hook. It comes with **caching, error & loading states, automatic query invalidation & re-fetches, and no race conditions**. There are other data-fetching libraries like SWR, but **React Query has better flexibility with its features, is more performant, and has a larger community**.

###How does it look like as a solution?
Well now, React Query fixes those bugs better and your **code is more concise and easier to read**. React Query manages the complexities of **managing your state properly**.

```
import React, { useState } from 'react';
import { useQueryClient, useQuery, useMutation } from 'react-query';

const categories = ['Electronics', 'Clothing', 'Books'];

function ProductListComponent() {
  const queryClient = useQueryClient();
  const [selectedCategory, setSelectedCategory] = useState(null);

  // Query to fetch products
  const fetchProducts = async (category) => {
    const response = await fetch(`https://api.example.com/products?category=${category}`);
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  };

  // Mutation to fetch products and invalidate the query
  const mutation = useMutation(fetchProducts, {
    onSuccess: (data, variables) => {
      // Invalidate and refetch
      queryClient.setQueryData(['products', variables], data);
      queryClient.invalidateQueries(['products', variables]);
    },
  });

  // Query to get products
  const { data: products, isLoading, isError } = useQuery(
    ['products', selectedCategory],
    () => fetchProducts(selectedCategory),
    {
      enabled: !!selectedCategory, // Only fetch when selectedCategory is truthy
      staleTime: 5 * 60 * 1000, // Cache data for 5 minutes
    }
  );

  // Handler for category selection
  const handleCategoryClick = (category) => {
    setSelectedCategory(category);
    mutation.mutate(category);
  };

  return (
    <div className="product-list-container">
      <h1>Product List</h1>
      <div>
        {categories.map(category => (
          <button key={category} onClick={() => handleCategoryClick(category)}>
            {category}
          </button>
        ))}
      </div>
      {isLoading && <p>Loading...</p>}
      {isError && <p>Error fetching data</p>}
      {products && (
        <ul>
          {products.map(product => (
            <li key={product.id}>
              <h2>{product.name}</h2>
              <p>Price: ${product.price}</p>
              <p>{product.description}</p>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default ProductListComponent;

```

---

At least now, you can see **why the `useEffect` hook isn't the best thing** when **managing async state** in your app. It might be okay for fetching data, but it doesn't have enough features to make **state predictable**. So you can fetch your data without React Query. But with React Query your **code and your state become more maintainable**.

---


## Resources
https://dev.to/amrguaily/useeffect-some-issues-with-data-fetching-in-effects-21nn
https://dev.to/sakethkowtha/react-query-vs-useswr-122b
https://tkdodo.eu/blog/why-you-want-react-query
https://medium.com/@omar1.mayallo4/react-hooks-useeffect-problems-in-data-fetching-5e2abc37a1c9
https://www.youtube.com/watch?v=SYs5E4yrtpY

---

You can hear more from me on:
[Twitter (X)](https://x.com/code_withjoseph) | [Instagram](https://www.instagram.com/codewithjosephwebdev)