Skip to content
React core 3 min read

useEffect & Data Fetching

useEffect lets a component synchronize with systems outside React—the network, timers, browser APIs, subscriptions. It’s also the most misunderstood Hook. This page covers how it actually works and how to fetch data with it correctly.

Mechanics: effects run after render

React renders your component (computing JSX), commits to the DOM, paints the screen, and then runs your effects. Effects never block the browser from painting.

useEffect(() => {
  document.title = `Inbox (${count})`;
});

With no dependency array, this runs after every render. That’s rarely what you want—use the dependency array to scope it.

The dependency array

The second argument tells React when to re-run the effect by comparing each dependency to its previous value:

useEffect(() => { /* ... */ }, [userId]); // runs when userId changes
useEffect(() => { /* ... */ }, []);        // runs once, on mount
useEffect(() => { /* ... */ });            // runs after every render

List every value the effect reads from component scope—props, state, functions. Lying to the dependency array causes stale closures, where the effect captures an old value and silently misbehaves. The lint plugin flags omissions.

Cleanup

If an effect creates something that must be torn down—a subscription, timer, or listener—return a cleanup function. React runs it before the next effect run and on unmount.

useEffect(() => {
  function onResize() { setWidth(window.innerWidth); }
  window.addEventListener("resize", onResize);
  return () => window.removeEventListener("resize", onResize);
}, []);

Forgetting cleanup is the classic source of memory leaks and duplicate subscriptions.

Why effects run twice in StrictMode

In development, React’s <StrictMode> deliberately mounts, unmounts, and remounts each component once, so every effect runs setup → cleanup → setup. This isn’t a bug—it’s a stress test that surfaces effects missing proper cleanup. If your effect is correctly symmetric (every setup has a matching teardown), the double-invoke is harmless. It does not happen in production builds.

Data fetching with loading and error state

A robust fetch effect tracks three things: the data, a loading flag, and an error. Here it is in TypeScript:

interface User { id: string; name: string; }

function UserCard({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    setError(null);

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data: User) => setUser(data))
      .catch((err) => {
        if (err.name !== "AbortError") setError(err.message);
      })
      .finally(() => setLoading(false));

    return () => controller.abort(); // cancel on unmount or userId change
  }, [userId]);

  if (loading) return <Spinner />;
  if (error)   return <p role="alert">Failed: {error}</p>;
  return <h2>{user?.name}</h2>;
}

AbortController and race conditions

When userId changes rapidly, multiple requests are in flight at once. Without cancellation, a slow earlier response can resolve after a faster later one and overwrite it—a race condition. The cleanup function calls controller.abort(), canceling the stale request; we ignore the resulting AbortError. This is essential for correctness, not just tidiness.

When not to use an effect

Effects are for synchronizing with external systems—not for transforming data for rendering. Common anti-patterns:

Instead of an effect that…Do this
Computes a value from props/stateCalculate it during render
Resets state when a prop changesUse a key to remount the component
Handles a user action (e.g. POST on click)Put the logic in the event handler

For real applications, prefer a dedicated data library—TanStack Query or SWR—which handle caching, deduplication, retries, and race conditions for you. The manual pattern above is what they’re built on.

Best Practices

  • Include every dependency the effect reads; trust the lint rule.
  • Always return cleanup for subscriptions, timers, and listeners.
  • Use AbortController to cancel in-flight requests and prevent races.
  • Keep <StrictMode> on in development—it exposes missing cleanup early.
  • Don’t use effects for data you can derive during render or logic that belongs in event handlers.
  • For production data fetching, reach for TanStack Query or SWR instead of hand-rolling.
Last updated June 1, 2026
Was this helpful?