Data Fetching

Fetching data sounds simple until you hit the real requirements: caching, loading and error states, refetching on focus, deduping concurrent requests, pagination, and keeping the UI in sync with the server. Hand-rolling all of that with useEffect and useState is where bugs breed.

The key insight is that server data is not your app's state — it's a cache of data that lives elsewhere and can go stale. A query library manages that cache for you, which is why useEffect-based fetching has largely been replaced.

TL;DR

Quick Example

A cached query with TanStack Query — deduped, refetched, and cached automatically:

Core Concepts

Server state vs client state

Client state is owned by your app (UI toggles, form input). Server state is a cached copy of remote data with its own concerns — freshness, refetching, invalidation. Mixing them is the most common data-layer mistake. See State Management.

Caching knobs

The three UI states

Every fetch should render loading, error, and empty branches — not just the happy path:

Patterns

Optimistic updates

Apply the change immediately, roll back on error — the UI feels instant:

Infinite scroll

useInfiniteQuery with a getNextPageParam handles cursor-based pagination and accumulation.

Comparison

Common Mistakes

Fetching in useEffect by hand

Copying server data into client state

FAQ

Why not just use useEffect and fetch?

Because you'd reimplement caching, deduping, refetch-on-focus, retries, and race-condition handling — badly. Query libraries give you all of that declaratively, which is why manual useEffect fetching is now an anti-pattern for server data.

TanStack Query or SWR?

TanStack Query for feature-rich apps (mutations, infinite queries, devtools, fine-grained caching). SWR for simpler, read-heavy apps and tight Next.js integration. Both implement stale-while-revalidate at heart.

What's the difference between staleTime and gcTime?

staleTime is how long data is considered fresh (no background refetch). gcTime is how long unused data stays in the cache before it's garbage-collected. They control freshness and memory respectively.

What are optimistic updates?

Updating the UI immediately on a mutation — before the server responds — then rolling back if it fails. It makes interactions feel instant, at the cost of handling the rollback path.

Related Topics

References