DevAcademy
LearnReactData Fetching Patterns
AdvancedReact

Data Fetching Patterns

Learn the common ways to fetch, cache, and manage server data in a React application.

Reading Time

18 min

Lesson

Lesson 38 of 42

Fetching in useEffect: The Basic Approach

The most direct approach fetches data in a useEffect on mount, tracking loading and error state manually. It works, but every component re-implements the same loading/error/caching logic from scratch.

Manual Fetching with useEffect

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;

    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (!cancelled) {
          setUser(data);
          setLoading(false);
        }
      })
      .catch((err) => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });

    return () => { cancelled = true; }; // avoid setting state after unmount
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong.</p>;
  return <h2>{user.name}</h2>;
}

Why the "cancelled" Flag Matters

If userId changes quickly (like a user clicking through several profiles fast), an older, slower request could resolve after a newer one, overwriting fresh data with stale data. Tracking whether the effect has been cleaned up prevents that race condition.

Data-Fetching Libraries

Libraries like TanStack Query (React Query) and SWR handle caching, deduplication, background refetching, and race conditions out of the box, removing the need to hand-write the pattern above in every component.

Fetching with TanStack Query

import { useQuery } from "@tanstack/react-query";

function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useQuery({
    queryKey: ["user", userId],
    queryFn: () => fetch(`/api/users/${userId}`).then((res) => res.json()),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong.</p>;
  return <h2>{user.name}</h2>;
}

What These Libraries Add

FeatureBenefit
Caching by keyAvoids re-fetching the same data across components
Automatic deduplicationMultiple components requesting the same data trigger one request
Background refetchingKeeps data fresh without a full loading state
Built-in race condition handlingNo manual "cancelled" flag needed

Framework-Level Data Fetching

In frameworks with Server Components (like Next.js’s App Router), Server Components can fetch data directly with async/await during rendering — no useEffect, loading state, or client-side request needed at all for that initial data.

Fetching Directly in a Server Component

async function ProductPage({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`).then(
    (res) => res.json()
  );

  return <h1>{product.name}</h1>;
}
// No loading state needed — the page isn't sent to the browser until this resolves

Best Practice

For a plain client-side React app, reach for a data-fetching library (TanStack Query or SWR) over hand-rolled useEffect fetching as soon as the app has more than a couple of data-dependent components — the caching and race-condition handling alone are worth it.

Interview Questions

Quick Quiz

1. Why does the manual useEffect fetch example track a "cancelled" flag?

2. What is one benefit of a library like TanStack Query over manual fetching?

3. How can a Server Component fetch data, compared to a Client Component?