Dung (Donny) Nguyen

Senior Software Engineer

Data Fetching Libraries

Fetching data is one of the most common tasks in a React application. While we can do it with useEffect and the browser’s fetch API, that approach quickly becomes repetitive and error‑prone once we need caching, background updates, retries, and loading/error state management. Data fetching libraries solve these problems by giving us a declarative, cache‑aware way to work with server state.

The two most popular options in the React ecosystem are TanStack Query (React Query) and SWR.

Why Not Just Use useEffect + fetch?

A typical hand‑rolled fetch looks like this:

import { useEffect, useState } from 'react';

function Users() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let ignore = false;
    setLoading(true);
    fetch('/api/users')
      .then((res) => res.json())
      .then((json) => {
        if (!ignore) setData(json);
      })
      .catch((err) => {
        if (!ignore) setError(err);
      })
      .finally(() => {
        if (!ignore) setLoading(false);
      });
    return () => {
      ignore = true;
    };
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong.</p>;
  return <ul>{data.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}

This works, but we have to repeat this pattern everywhere and manually handle:

Data fetching libraries handle all of this for us.

Server State vs. Client State

A key idea behind these libraries is the distinction between two kinds of state:

React Query and SWR specialize in server state, which behaves very differently from local UI state.

TanStack Query (React Query)

TanStack Query is a powerful, feature‑rich library for fetching, caching, synchronizing, and updating server state.

Setup

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Users />
    </QueryClientProvider>
  );
}

Fetching Data with useQuery

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

function Users() {
  const { data, isLoading, isError } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then((res) => res.json()),
  });

  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>Something went wrong.</p>;

  return (
    <ul>
      {data.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

The queryKey uniquely identifies the query so React Query can cache and dedupe it. That single line replaces all the manual state management above.

Mutations with useMutation

For creating, updating, or deleting data, React Query provides useMutation:

import { useMutation, useQueryClient } from '@tanstack/react-query';

function AddUser() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (newUser) =>
      fetch('/api/users', {
        method: 'POST',
        body: JSON.stringify(newUser),
      }).then((res) => res.json()),
    onSuccess: () => {
      // Refetch the users list after a successful add.
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });

  return (
    <button onClick={() => mutation.mutate({ name: 'Alice' })}>
      Add User
    </button>
  );
}

Key Features

SWR

SWR (from Vercel, the creators of Next.js) is a lightweight data fetching library. Its name comes from the stale‑while‑revalidate HTTP caching strategy: return cached (stale) data first, then fetch (revalidate) in the background, and finally update with fresh data.

Fetching Data with useSWR

import useSWR from 'swr';

const fetcher = (url) => fetch(url).then((res) => res.json());

function Users() {
  const { data, error, isLoading } = useSWR('/api/users', fetcher);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Something went wrong.</p>;

  return (
    <ul>
      {data.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

SWR uses the request URL (the first argument) as the cache key and accepts a fetcher function that returns the data.

Key Features

React Query vs. SWR

Aspect TanStack Query SWR
Size Larger, more features Lightweight, minimal
Mutations Built‑in useMutation Manual (via mutate)
Caching control Fine‑grained Simpler, opinionated
Devtools Rich, dedicated Devtools Basic
Best for Complex apps, heavy server state Simple reads, Next.js apps

Both libraries share the same core philosophy and can dramatically simplify data fetching. Choose React Query when you need advanced control over caching and mutations, and SWR when you want a small, simple solution—especially in a Next.js project.

Best Practices

Conclusion

Data fetching libraries turn tedious, repetitive fetch logic into a few declarative lines while giving us caching, deduplication, background updates, and error handling for free. For most modern React applications, reaching for TanStack Query or SWR instead of raw useEffect + fetch leads to cleaner, faster, and more maintainable code.