---
title: "12 — Data Fetching in React — TanStack Query, SWR, and Why a Cache Beats a Fetch"
uid: data-fetching
tags: ["relay", "react", "urql", "roadmap:react", "rtk-query", "tanstack-query", "swr", "axios", "apollo"]
excerpt: "The moment you need caching, deduplication, loading states, retries, or mutation sync across components, a data-fetching library earns its keep — raw fetch does none of that."
date: 2026-08-13T03:27:44+0000
source: https://www.aveshina.my.id/en/blog/data-fetching
---

"Just use fetch" stopped being enough almost immediately in React, and the reimplementation cost taught me the boundary. The framing that organized it: **the moment I need caching, request deduplication, loading/error states, retries, background refresh, or mutation sync across components, a dedicated data-fetching library earns its keep.** [1][2] Raw fetch in a useEffect does none of that, and reimplementing each piece by hand is how half-finished data layers are born.

## Why fetch in useEffect isn't enough

The starting point almost everyone writes first:

```
useEffect(() => {
  fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
```

This works for a hello-world. The moment it hits a real app it breaks down, because it doesn't handle:

- **Deduplication.** Two components mounting both fire the same /api/users request — a duplicate round trip.
- **Caching.** Navigating away and back re-fetches, even if the data hasn't changed.
- **Loading and error states.** I'm hand-rolling isLoading, error, and data flags every time.
- **Background refresh.** Stale-while-revalidate, refetch on focus, polling — all DIY.
- **Race conditions.** A late response overwriting a newer one because I didn't track the latest request.
- **Mutations.** After a POST, I have to manually refetch or update local state across every component that read that data.

Each of those is a real problem I'll hit. A data-fetching library solves them as a bundle.

## TanStack Query: server state as a first-class concern

TanStack Query (formerly React Query) is the dominant answer for REST-style data [1]. Its core move is to treat **server state** as distinct from client state — fetched data lives in a query cache keyed by an identifier, and components subscribe to that cache rather than holding their own copy.

```
const { data, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: () => fetch('/api/users').then(r => r.json()),
});
```

Out of the box, this one hook gives me: request deduplication (two components with the same queryKey share one request), a cache (navigating back returns cached data instantly and revalidates in the background), loading and error states, automatic retries, and stale-while-revalidate on window focus. Mutations work through useMutation, and queryClient.invalidateQueries(['users']) after a mutation triggers a refetch for every component reading users — the cross-component sync problem disappears.

```figure
<svg viewBox="0 0 740 300" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Two models. Left: three components each fire their own fetch to /api/users — three duplicate requests, each manages its own loading/error state. Right: the same three components subscribe to one query cache (TanStack Query) keyed by ['users']; one request fires, results are shared and cached.">
  <defs>
    <marker id="darrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- LEFT -->
    <text x="150" y="26" font-size="12" font-weight="700" fill="#7f1d1d" text-anchor="middle">fetch in useEffect — duplicate</text>

    <rect x="40" y="50" width="80" height="36" rx="6" fill="#fee2e2" stroke="#dc2626"/>
    <text x="80" y="72" font-size="10" fill="#7f1d1d" text-anchor="middle">Comp A</text>
    <rect x="40" y="100" width="80" height="36" rx="6" fill="#fee2e2" stroke="#dc2626"/>
    <text x="80" y="122" font-size="10" fill="#7f1d1d" text-anchor="middle">Comp B</text>
    <rect x="40" y="150" width="80" height="36" rx="6" fill="#fee2e2" stroke="#dc2626"/>
    <text x="80" y="172" font-size="10" fill="#7f1d1d" text-anchor="middle">Comp C</text>

    <rect x="180" y="90" width="100" height="56" rx="6" fill="#fef9c3" stroke="#ca8a04"/>
    <text x="230" y="112" font-size="10" fill="#422006" text-anchor="middle">/api/users</text>
    <text x="230" y="128" font-size="9" fill="#422006" text-anchor="middle">3 requests</text>
    <text x="230" y="142" font-size="9" fill="#422006" text-anchor="middle">3 loading flags</text>

    <path d="M120,68 L178,100" stroke="#dc2626" stroke-width="1.5" marker-end="url(#darrow)"/>
    <path d="M120,118 L178,118" stroke="#dc2626" stroke-width="1.5" marker-end="url(#darrow)"/>
    <path d="M120,168 L178,136" stroke="#dc2626" stroke-width="1.5" marker-end="url(#darrow)"/>

    <text x="150" y="230" font-size="10" fill="#7f1d1d" text-anchor="middle" font-style="italic">no cache · no dedup</text>
    <text x="150" y="246" font-size="10" fill="#7f1d1d" text-anchor="middle" font-style="italic">manual refetch after mutation</text>

    <!-- divider -->
    <line x1="320" y1="40" x2="320" y2="260" stroke="#374151" stroke-dasharray="4 4"/>

    <!-- RIGHT -->
    <text x="530" y="26" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">TanStack Query — shared cache</text>

    <rect x="380" y="50" width="80" height="36" rx="6" fill="#e0e7ff" stroke="#6366f1"/>
    <text x="420" y="72" font-size="10" fill="#1e1b4b" text-anchor="middle">Comp A</text>
    <rect x="380" y="100" width="80" height="36" rx="6" fill="#e0e7ff" stroke="#6366f1"/>
    <text x="420" y="122" font-size="10" fill="#1e1b4b" text-anchor="middle">Comp B</text>
    <rect x="380" y="150" width="80" height="36" rx="6" fill="#e0e7ff" stroke="#6366f1"/>
    <text x="420" y="172" font-size="10" fill="#1e1b4b" text-anchor="middle">Comp C</text>

    <!-- cache -->
    <rect x="500" y="70" width="100" height="86" rx="6" fill="#dcfce7" stroke="#16a34a"/>
    <text x="550" y="92" font-size="10" font-weight="700" fill="#052e16" text-anchor="middle">query cache</text>
    <text x="550" y="110" font-size="9" font-family="ui-monospace,monospace" fill="#052e16" text-anchor="middle">['users']</text>
    <text x="550" y="130" font-size="9" fill="#052e16" text-anchor="middle">1 request · cached</text>
    <text x="550" y="146" font-size="9" fill="#052e16" text-anchor="middle">auto refetch</text>

    <path d="M460,68 L498,90" stroke="#16a34a" stroke-width="1.5" marker-end="url(#darrow)"/>
    <path d="M460,118 L498,113" stroke="#16a34a" stroke-width="1.5" marker-end="url(#darrow)"/>
    <path d="M460,168 L498,136" stroke="#16a34a" stroke-width="1.5" marker-end="url(#darrow)"/>

    <text x="530" y="200" font-size="10" fill="#1e1b4b" text-anchor="middle" font-style="italic">dedup · cache · stale-while-revalidate</text>
    <text x="530" y="216" font-size="10" fill="#1e1b4b" text-anchor="middle" font-style="italic">invalidateQueries syncs every reader</text>
  </g>
</svg>
```

## SWR: the stale-while-revalidate original

SWR (from Vercel) is the other main REST-side option, named after the HTTP cache invalidation strategy **stale-while-revalidate** [2]. Its model is similar to TanStack Query's: a hook returns cached data first (stale), fires a background request (revalidate), then returns the up-to-date data. With one hook, the boilerplate of loading flags, error handling, and refetch logic collapses.

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

SWR is smaller and simpler than TanStack Query — fewer features, less API surface. For straightforward data fetching where I don't need the full mutation/invalidation machinery, SWR is lighter. TanStack Query is the choice when mutations, optimistic updates, and fine-grained cache control become central; the two share the same fundamental insight (server state in a cache, components subscribe).

## Axios: the HTTP client layer

Axios sits below the cache layer — it's an HTTP **client**, not a data-fetching library [3]. It's a popular alternative to the native fetch API, offering interceptors (global request/response transforms), automatic JSON parsing, request cancellation, and a more ergonomic API than raw fetch. The roadmap lists it under API calls [3], and the way I use it: as the queryFn/fetcher implementation inside TanStack Query or SWR, not as a standalone data layer. Axios handles the HTTP mechanics; the cache library handles the React integration.

## The GraphQL clients: Apollo, Relay, urql

When the API is GraphQL rather than REST, a GraphQL-specific client takes the cache-library job. The frontend notes cover GraphQL in depth; for these notes, the React-side mapping:

- **Apollo Client** is the general-purpose default — caching, queries, mutations, loading state, with strong React bindings [4]. The normalized cache deduplicates entities across queries. For most React + GraphQL apps, Apollo is the "pick this one" answer.
- **Relay** is Meta's client, built for data-heavy apps at scale, with co-located fragments compiled at build time [5]. Powerful, but a strict way of thinking and a mandatory build step.
- **urql** is the lightweight alternative — simpler and smaller than Apollo, with a flexible extensible architecture [6]. Good for small-to-medium apps where Apollo's surface is too much.

The decision is the same shape as the REST side: what's the equivalent of "cache + loading + mutation sync" for my data source? For REST it's TanStack Query or SWR; for GraphQL it's Apollo/Relay/urql. The GraphQL clients fold the HTTP layer in too, so Axios isn't in the picture there.

## RTK Query: Redux Toolkit's bundled answer

RTK Query is worth naming because it's the option if I'm already in Redux [7]. It's a data-fetching and caching tool built into Redux Toolkit, designed to simplify fetching, caching, polling, and invalidation — the same problem space as TanStack Query, integrated with the Redux store. If the project already uses Redux, RTK Query is the natural fit; if not, I'd reach for TanStack Query rather than adopting Redux just to get it.

## How I use this

The way of thinking I run is "server state is not client state." Fetched data lives in a cache, and components subscribe to that cache — they don't each hold their own copy in useState. For REST APIs, TanStack Query is my default: it handles the full lifecycle, and after a mutation I call invalidateQueries rather than threading updates through the component tree. For GraphQL, Apollo Client covers the same ground. SWR is my pick for simpler fetches where the full TanStack Query API would be overkill. Axios is the HTTP client underneath, when I need its interceptors or ergonomics over raw fetch. The thing I never do anymore is fetch in a bare useEffect for anything beyond a throwaway — every one of those is a future bug report about loading spinners, stale data, or duplicate requests.

## References

[1] TanStack, "TanStack Query — powerful asynchronous state management," tanstack.com, 2024. [Online]. Available: [https://tanstack.com/query/latest](https://tanstack.com/query/latest)

[2] Vercel, "SWR — React hooks for data fetching," swr.vercel.app, 2024. [Online]. Available: [https://swr.vercel.app/](https://swr.vercel.app/)

[3] Axios, "Axios — getting started," axios-http.com, 2024. [Online]. Available: [https://axios-http.com/docs/intro](https://axios-http.com/docs/intro)

[4] Apollo GraphQL, "Get started with Apollo Client," apollographql.com, 2024. [Online]. Available: [https://www.apollographql.com/docs/react/](https://www.apollographql.com/docs/react/)

[5] Meta, "Relay — a JavaScript framework for building data-driven React applications," relay.dev, 2024. [Online]. Available: [https://relay.dev/](https://relay.dev/)

[6] Formidable Labs, "urql — Universal React Query Library," formidable.com, 2024. [Online]. Available: [https://formidable.com/open-source/urql/](https://formidable.com/open-source/urql/)

[7] Redux team, "RTK Query — overview," redux-toolkit.js.org, 2024. [Online]. Available: [https://redux-toolkit.js.org/rtk-query/overview](https://redux-toolkit.js.org/rtk-query/overview)

```quiz
Q: The core problem with raw fetch in a useEffect is:
- it can't make HTTP requests
- it doesn't deduplicate, cache, manage loading/error states, or sync mutations across components
- it only works with class components
correct: 1
explain: Raw fetch handles the request but none of the surrounding concerns — dedup, cache, retries, stale-while-revalidate, race-condition handling, or cross-component mutation sync. A data-fetching library solves these as a bundle.

Q: TanStack Query deduplicates requests by:
- sharing one cached response per query key across all components using it
- making every component fetch sequentially
- disabling concurrent mounts
correct: 0
explain: Components with the same queryKey share one request and one cached response, so two mounts of the same query fire a single round trip.

Q: SWR's name comes from:
- a JavaScript keyword
- the stale-while-revalidate HTTP cache strategy — return cached data first, then revalidate in the background
- an acronym for "sync with redux"
correct: 1
explain: SWR returns stale cached data immediately, fires a background revalidation, then updates with fresh data — the stale-while-revalidate pattern.

Q: After a successful mutation in TanStack Query, how do you sync every component reading that data?
- manually setState in each component
- call queryClient.invalidateQueries(['key']) to trigger a refetch for every reader of that key
- reload the page
correct: 1
explain: Invalidation is the whole point — one call marks the cached query stale and triggers a refetch for every subscribed component, replacing manual cross-tree updates.

Q: When the API is GraphQL, the cache-library role is filled by:
- TanStack Query (it only handles REST)
- a GraphQL client like Apollo, Relay, or urql — which fold in caching, mutations, and the HTTP layer
- Axios alone
correct: 1
explain: GraphQL clients own the cache/mutation/loading-state job for GraphQL data, and they include the HTTP transport. TanStack Query is REST-shaped; Apollo/Relay/urql are their GraphQL counterparts.
```
