TanStack Query

TanStack Query (still widely known as React Query, and available for React, Vue, Svelte, Solid, and Angular) is the de facto library for managing server state in frontend applications — the data that lives on your backend and is fetched, cached, and synchronized to the UI. It replaces the fragile, repetitive pattern of fetching data in a useEffect with loading/error/data state you wire by hand, and it does far more: caching, background refetching, deduplication, pagination, and optimistic updates, mostly automatically.

Its central insight is that server state is fundamentally different from client state and deserves its own tool. Client state (a modal's open/closed, a form's draft) is owned by your app and synchronous. Server state is owned elsewhere, asynchronous, can go stale, and is shared across components — properties that make it a poor fit for the state tools built for client state. TanStack Query is purpose-built for it.

TL;DR

Quick Example

useQuery replaces an entire useEffect fetching pattern with one declarative hook that gives you cached data, loading, and error states — plus caching and background refetching for free:

Compare that to the useEffect version — a useState for data, one for loading, one for error, a fetch, cleanup to avoid setting state after unmount, and no caching or dedup. TanStack Query collapses all of it.

Core Concepts

Server State vs Client State

The framing that explains why this library exists:

Trying to manage server state with client-state tools (dumping fetched data into a global store) means reinventing caching, refetching, and invalidation by hand. TanStack Query provides those as the whole point.

Queries and Query Keys

A query is a declarative dependency on some async data, defined by a query key (like ["user", id]) and a fetch function. The key is how the cache identifies data: the same key returns the cached result; a changed key (e.g. a different id) fetches new data. Keys also drive invalidation — you can say "refetch everything under ["user"]" after a change.

Stale-While-Revalidate

TanStack Query's caching model: data has a stale time during which it's served instantly from cache with no refetch. Once stale, the cache is still shown immediately (so the UI is fast), but a background refetch updates it quietly. This "stale-while-revalidate" behavior is why apps built on it feel instant and stay fresh — you rarely see a spinner for data you've seen before. It also refetches on triggers like window refocus and reconnect, keeping data live.

Mutations and Invalidation

Reads are queries; writes are mutations (useMutation). After a mutation succeeds, you invalidate the affected queries so they refetch and the UI reflects the change. For snappier UX, optimistic updates apply the change to the cache immediately and roll back if the mutation fails.

Best Practices

Use It for Server State, Not Everything

TanStack Query is not a Redux replacement for all state. Use it for remote data; keep genuinely-client state (UI toggles, form drafts, ephemeral selections) in useState or a client-state manager. Mixing the two into one tool is where confusion starts.

Set staleTime Deliberately

The default treats data as immediately stale, causing frequent background refetches. For data that doesn't change every second, raise staleTime to cut needless refetching and network chatter. Tune it per query to how fresh that data truly needs to be.

Structure Query Keys Consistently

Query keys are your cache's identity and invalidation surface. Use a consistent, hierarchical convention (["user", id], ["users", { filters }]) so you can invalidate broadly or narrowly. Many teams centralize key definitions to avoid typos and mismatches.

Invalidate After Mutations

The most common consistency bug is a write that doesn't update the UI. After a mutation, invalidate the queries it affects (or update the cache directly). This keeps the displayed data consistent with the server without manual refetch wiring.

Reach for Optimistic Updates on Hot Paths

For interactions where latency hurts UX (likes, toggles, reordering), apply optimistic cache updates so the UI responds instantly, with rollback on failure. Use them where they matter; the extra code isn't worth it everywhere.

Common Mistakes

Recreating It With useEffect

Putting Server Data in a Global Client Store

Copying fetched data into Redux/Zustand and managing freshness by hand rebuilds — worse — what TanStack Query already does. Let it own server state; use the client store for client state.

Forgetting to Invalidate

A successful mutation that doesn't invalidate leaves the UI showing stale data until the next natural refetch. Invalidate (or update) the relevant queries in onSuccess.

FAQ

Is TanStack Query a state management library like Redux?

No — and conflating them is the main confusion. Redux and similar tools manage client state your app owns. TanStack Query manages server state — remote data that's async, cacheable, and can go stale. They're complementary: many apps use TanStack Query for all server data and a small amount of client-state tooling (or just useState) for UI state. It replaces the data-fetching parts of a global store, not the store itself.

What's the difference between React Query and TanStack Query?

They're the same library. It began as React Query, then expanded to support Vue, Svelte, Solid, and Angular, and was renamed TanStack Query to reflect that it's framework-agnostic. In React projects people still call it React Query; the package is @tanstack/react-query.

What does staleTime do?

staleTime is how long fetched data is considered fresh. While fresh, TanStack Query serves it from cache with no refetch. Once stale, it still shows the cached data instantly but refetches in the background to update it (stale-while-revalidate). The default is 0 (immediately stale), which causes frequent refetches — raising it for data that changes slowly reduces unnecessary network traffic.

How do I keep the UI in sync after a write?

Use a mutation (useMutation) for the write, then invalidate the affected queries in onSuccess so they refetch fresh data. For instant feedback, apply an optimistic update to the cache before the server responds and roll it back on failure. Invalidation is the key step that keeps displayed data consistent with the server.

Do I still need it with server components or a framework's data layer?

It depends. Frameworks with their own data fetching (like server components or route loaders) cover the initial load well, but TanStack Query still shines for client-side interactivity: caching across navigation, background refetching, mutations with optimistic updates, and live data (refetch on focus/reconnect). Many apps combine framework-level fetching for first render with TanStack Query for client-side server-state management.

Related Topics

References