Data Fetching

This section documents React 19.x for the web, using function components and Hooks throughout (class components appear only where React still requires them, such as error boundaries). React Native is out of scope and gets only a pointer. The React 19 additions follow the current official React documentation. This content was generated with the assistance of AI and should be verified against react.dev before being relied on in production, since React APIs continue to evolve between releases.

This section’s bibliography lists the reference material consulted while preparing these pages.

React does not prescribe how you fetch data. The options range from a bare fetch in an Effect to a purpose-built caching library to framework-level loading. For a real app, use a library or a framework.

fetch in an Effect: the baseline

function Product({ id }) {
  const [product, setProduct] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    let ignore = false;
    fetch(`/api/products/${id}`)
      .then((r) => { if (!r.ok) throw new Error(r.status); return r.json(); })
      .then((data) => { if (!ignore) setProduct(data); })
      .catch((e) => { if (!ignore) setError(e); });
    return () => { ignore = true; };
  }, [id]);

  if (error) return <p role="alert">Failed to load.</p>;
  if (!product) return <Spinner />;
  return <ProductView product={product} />;
}

Its problems: request waterfalls (child fetches only start after the parent renders), race conditions (handled here with the ignore flag or an AbortController), no cache (every mount refetches), and no shared loading/error conventions. See Effects for the Effect mechanics, Networking for fetch itself, and — for cross-origin requests — What is CORS?.

axios and server-state libraries

axios is a small HTTP client with interceptors, automatic JSON, and a terser API than fetch; it does not solve caching or races.

TanStack Query (and SWR, and RTK Query) treat server data as a cache: they deduplicate concurrent requests, cache by key, revalidate in the background (on focus, on reconnect, on interval), and expose mutations with cache invalidation.

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

function Product({ id }) {
  const { data, isPending, error } = useQuery({
    queryKey: ['product', id],
    queryFn: () => fetch(`/api/products/${id}`).then((r) => r.json()),
    staleTime: 60_000,
  });

  if (isPending) return <Spinner />;
  if (error) return <p role="alert">Failed to load.</p>;
  return <ProductView product={data} />;
}

GraphQL and framework loading

For GraphQL APIs, a client such as Apollo Client, urql, or Relay provides the query cache and normalized store equivalent to the above. For a deeper reference on configuring these clients — Apollo Client, urql and Relay — see GraphQL Reference: configuring clients.

The modern answer for many apps is to fetch on the server: a framework route loader or a React Server Component fetches data before render, eliminating the client waterfall and shipping less JavaScript.

sequenceDiagram participant C as Component participant Q as Query hook participant Ca as Cache participant S as Server C->>Q: useQuery product id Q->>Ca: lookup key alt cache hit and fresh Ca-->>C: cached data (no request) else miss or stale Q->>S: GET /api/products/:id S-->>Q: JSON Q->>Ca: store under key Q-->>C: data, then re-render end