Skip to main content
Back to module
React Fundamentals · Effects and Data

useEffect and data fetching

Reading40mLesson 1 of 3 in this module

What you'll learn

  • Fetch data with loading and error states
  • Control when an effect runs with its dependency array
  • Clean up timers, subscriptions and stale requests in the effect's return function
  • Recognise when an effect is not needed

Run effects after render, clean them up, and fetch data with clear loading, success and error states.

Why it matters

Rendering should be pure, but apps must talk to the outside world: load data from an API, start a timer, subscribe to a WebSocket, set the page title. useEffect is how a component synchronises with something outside React after it renders. Loading data with clear loading and error states is one of the most common tasks in front-end work.

The shape of an effect

JSX
React.useEffect(() => {
  // runs after React has updated the screen
  document.title = `${count} new messages`;

  return () => {
    // optional cleanup: runs before the effect runs again, and on unmount
  };
}, [count]); // dependency array

The dependency array controls when the effect runs:

DependenciesRuns
[count]after the first render, then whenever count changes
[]once, after the first render (on mount)
noneafter every render (rarely what you want)

List every prop and state value the effect reads. The React ESLint plugin (react-hooks/exhaustive-deps) warns when one is missing.

Cleanup

Anything an effect starts, its cleanup should stop:

JSX
React.useEffect(() => {
  const id = setInterval(() => setSeconds((s) => s + 1), 1000);
  return () => clearInterval(id);
}, []);

Without the cleanup, every remount would add another interval. In development, React's <StrictMode> deliberately mounts, unmounts and remounts components once to expose missing cleanups, so you may see effects run twice in development. That is expected; fix the cleanup rather than fighting it.

Fetching data with loading and error states

A request can be pending, succeed or fail, and the UI should show each case:

JSX
function UserList() {
  const [users, setUsers] = React.useState([]);
  const [status, setStatus] = React.useState("loading"); // "loading" | "done" | "error"

  React.useEffect(() => {
    let ignore = false;

    fetch("https://api.example.com/users")
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data) => {
        if (!ignore) {
          setUsers(data);
          setStatus("done");
        }
      })
      .catch(() => {
        if (!ignore) setStatus("error");
      });

    return () => {
      ignore = true;
    };
  }, []);

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

Key points:

  • fetch does not reject on HTTP errors. A 404 or 500 still resolves; check res.ok and throw yourself.
  • The ignore flag prevents a race. If the component unmounts, or the effect re-runs because a dependency (such as a search term) changed, the cleanup marks the old request as stale so its late answer does not overwrite newer data. AbortController can cancel the request itself: pass { signal: controller.signal } to fetch and call controller.abort() in the cleanup.
  • One status value is clearer than separate isLoading and hasError booleans that can contradict each other.
  • The effect callback itself cannot be async (it must return a cleanup function or nothing). Define an async function inside it and call it, or use .then chains as above.

Fetching when something changes

JSX
React.useEffect(() => {
  let ignore = false;
  setStatus("loading");
  fetch(`/api/products?q=${encodeURIComponent(query)}`)
    .then((r) => r.json())
    .then((data) => { if (!ignore) { setProducts(data); setStatus("done"); } })
    .catch(() => { if (!ignore) setStatus("error"); });
  return () => { ignore = true; };
}, [query]);

Each new query starts a request; the cleanup ignores the previous one.

Making effects testable

Passing the data-loading function in as a prop (or importing it from a module you can mock) lets tests supply fake data, a never-ending promise or a failure. The Do exercise works this way, because the exercise preview has no network.

You might not need an effect

Effects are for synchronising with external systems. You do not need one to:

  • Compute values from props or state. Calculate them during render (const visible = items.filter(...)).
  • Respond to a click. Put that logic in the event handler.
  • Reset state when a prop changes. Give the component a key so React remounts it.

In larger apps, data fetching usually moves to a framework (Next.js data loading) or a library such as TanStack Query, which adds caching, retries and deduplication. They still follow the same pending/success/error model you are learning here.

Common mistakes

  • Missing dependencies, so the effect uses stale values; or an object or function created during render in the dependency list, so the effect runs every render.
  • Setting state that is also a dependency, creating an infinite loop.
  • Forgetting cleanup for timers, subscriptions and listeners.
  • Treating any fetch response as success.

Recap

  • useEffect(fn, deps) runs after render to sync with the outside world; return a cleanup.
  • Model requests as loading, success and error, and render each.
  • Use an ignore flag or AbortController to drop stale responses.
  • Do not use effects for derived data or for event logic.

Try it

Open the Do tab to load users in an effect and show loading, list and error states. The checks pass in their own fake loaders, including one that fails.

Key terms (5)

useEffect
A hook that runs code after React renders, to synchronise a component with something outside React.
dependency array
The list passed to useEffect that decides when it re-runs: after mount only for [], or whenever a listed value changes.
cleanup
The function an effect returns; React calls it before the effect runs again and when the component unmounts.
race
A bug where an older request finishes after a newer one and overwrites its result; an ignore flag set in cleanup prevents it.
AbortController
A browser API whose signal can be passed to fetch so the request can be cancelled with abort().

Recap

  • useEffect(fn, deps) runs after render to sync with the outside world; return a cleanup.
  • List every prop and state value the effect reads in its dependency array.
  • Model requests as loading, success and error, and check res.ok because fetch does not reject on HTTP errors.
  • Use an ignore flag or AbortController to drop stale responses.
  • Do not use effects for derived data or click logic.
Also in this lesson: