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:
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 whenevercategorychanges. 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.
AbortControllergivesfetchasignal. Aborting makes thatfetchreject with an error namedAbortError, which you ignore because you caused it.statuslets 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.