Skip to content

React useEffect: dependencies, fetching and cleanup

November 1, 2026 · 2 min read

useEffect runs your code after React updates the page, and its dependency array decides when it runs again. To fetch products, call fetch inside the effect, list the values it uses (like category) in the array, and return a cleanup function that aborts the request.

Fetch on mount, cancel in cleanup

import { useEffect, useState } from "react";

export default function Products({ category }) {
  const [products, setProducts] = useState([]);
  const [status, setStatus] = useState("loading");

  useEffect(() => {
    const controller = new AbortController();
    setStatus("loading");

    fetch(`/api/products?category=${category}`, { signal: controller.signal })
      .then((response) => response.json())
      .then((data) => {
        setProducts(data);
        setStatus("ready");
      })
      .catch((error) => {
        if (error.name !== "AbortError") setStatus("error");
      });

    return () => controller.abort();
  }, [category]);

  if (status === "loading") return <p>Loading...</p>;
  if (status === "error") return <p>Could not load products.</p>;
  return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

I ran this component with a fake fetch. It rendered with category="mugs", then changed to "kettles" before the first answer arrived:

Output
fetch mugs
aborted mugs
fetch kettles

The page ended up showing only "Steel kettle", the kettles result.

What each part does

  • [category] tells React to run the effect after the first render and again whenever category changes. An empty array [] would fetch only once.
  • The cleanup function (the one you return) runs before the next effect run and when the component unmounts. Here it aborts the old request.
  • AbortController gives fetch a signal. Aborting makes that fetch reject with an error named AbortError, which you ignore because you caused it.
  • status lets you show loading and error states instead of an empty page.

Without the abort, a slow "mugs" answer could arrive after the "kettles" one and replace it with the wrong list.

Common mistake

Passing an async function straight to useEffect:

useEffect(async () => {
  const response = await fetch("/api/products");
  setProducts(await response.json());
}, []);

An async function returns a promise, but React expects nothing or a cleanup function, so you get a warning and no cleanup. Define an async function inside the effect and call it, or use .then as above.

Also, don't leave out the dependency array. With no array, the effect runs after every render, and an effect that sets state will loop forever.

Fetch inside useEffect, list what it uses in the array, and return a cleanup that aborts the request so stale answers never reach your state.

More on Web Development