Server State

TanStack Query

TanStack Query is the missing data-fetching layer for React. It turns server data into a cache with loading, error, refetching and invalidation handled for you.

intermediate14 min readUpdated Sep 15, 2026
usePosts.ts
tsx
// usePosts.ts
import { useQuery } from "@tanstack/react-query";

async function fetchPosts() {
  const res = await fetch("/api/posts");
  if (!res.ok) throw new Error("Failed to fetch posts");
  return res.json();
}

export function usePosts() {
  return useQuery({
    queryKey: ["posts"],
    queryFn: fetchPosts,
  });
}
Formerly
React Query
Framework
React, Vue, Svelte, Solid
Read with
useQuery
Write with
useMutation
Cache key
queryKey
Devtools
Dedicated panel

Why it matters

Why server state needs its own tool

A real cache for server data

Requests are cached, deduplicated and shared across components, so the same data is fetched once.

Loading and error for free

Every query exposes status, loading, error and data, so you stop hand-writing those flags.

Fresh by policy

Stale time, refetch on focus and background updates keep data current without constant manual work.

The big picture

The three ideas behind TanStack Query

A cache keyed by query, a query function that fetches, and invalidation that keeps the cache honest.

Queries

Read

A query key plus a fetch function describes a piece of server data and its cache entry.

Mutations

Write

useMutation handles create, update and delete requests and can update the cache on success.

Invalidation

Sync

Mark cached data stale so it refetches, keeping the UI consistent after a change.

TanStack Query at a glance

The core of TanStack Query

useQuery

Subscribe to a cached piece of server data by key.

useMutation

Send create, update and delete requests with loading and error state.

queryKey

The identity of a cache entry, used to read, update and invalidate it.

staleTime

How long data is considered fresh before a background refetch.

Invalidation

Mark queries stale so they refetch when needed.

Devtools

Inspect every query, its state and the cache in a dedicated panel.

A short history

The rise of server-state management

  1. 2019

    React Query released

    A small library brings caching and server-state management to React data fetching.

    19
  2. 2021

    React Query 3

    Wider adoption and a mature API make it a common default.

    21
  3. 2022

    TanStack Query

    The library is renamed and expanded to Vue, Svelte and Solid.

    22
  4. 2023

    TanStack Query 5

    A simpler API, better TypeScript inference and a smaller bundle.

    23
  5. Today

    The standard for server state

    A widely used layer that pairs with any client-state library.

    Today

The complete guide

TanStack Query: Everything you need to know

What is TanStack Query?

TanStack Query, formerly React Query, is a server-state management library. It recognises that data fetched from an API is fundamentally different from the state you keep in useState. Server data is shared, cached, can go stale, and needs to be refetched. Managing that with effects and flags is tedious and bug-prone.

TanStack Query gives server data a proper cache. You describe how to fetch a piece of data and give it a key, and the library handles caching, deduplication, loading and error state, background refetching and invalidation. It is the missing data layer for React, and it pairs with any client-state library.

Queries

A query is defined by a key and a query function. The key identifies the cache entry; the function fetches the data.

// api.ts
async function fetchPost(id: string) {
  const res = await fetch(`/api/posts/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
// Post.tsx
import { useQuery } from "@tanstack/react-query";

export function Post({ id }: { id: string }) {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ["post", id],
    queryFn: () => fetchPost(id),
  });

  if (isLoading) return <p>Loading…</p>;
  if (isError) return <p>Error: {error.message}</p>;

  return <article>{data.title}</article>;
}

Every component that uses the same key shares one cache entry, so mounting the component twice fetches once. The status flags are provided, so you never write them yourself.

Query keys and the cache

The key is the identity of the cache entry, and it should include every variable the fetch depends on.

// keys.ts
useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
useQuery({ queryKey: ["post", id], queryFn: () => fetchPost(id) });
useQuery({
  queryKey: ["posts", { page, status }],
  queryFn: () => fetchPosts({ page, status }),
});

Because keys are arrays, you can invalidate a whole family with a prefix. Invalidating ["posts"] also invalidates ["posts", { page: 1 }], which makes cache updates predictable.

Freshness and refetching

Two options control when data is refetched.

// freshness.ts
useQuery({
  queryKey: ["posts"],
  queryFn: fetchPosts,
  staleTime: 60_000, // fresh for one minute
  gcTime: 5 * 60_000, // keep unused data for five minutes
});

staleTime is how long data is considered fresh; while fresh, no background refetch happens. gcTime is how long an unused entry stays in memory before being garbage collected. By default, queries refetch on mount, on window focus and on reconnect, which keeps the UI current without any manual work.

Mutations

useMutation handles writes: create, update and delete.

// AddPost.tsx
import { useMutation, useQueryClient } from "@tanstack/react-query";

export function AddPost() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (title: string) =>
      fetch("/api/posts", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ title }),
      }).then((r) => r.json()),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["posts"] });
    },
  });

  return (
    <button
      onClick={() => mutation.mutate("New post")}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? "Saving…" : "Add post"}
    </button>
  );
}

After a successful mutation, invalidating the affected queries tells every consumer to refetch. This keeps the server as the source of truth and avoids manual cache surgery.

Optimistic updates

For actions that should feel instant, update the cache before the server responds and roll back on failure.

// optimistic.ts
useMutation({
  mutationFn: toggleLike,
  onMutate: async (postId) => {
    await queryClient.cancelQueries({ queryKey: ["post", postId] });
    const previous = queryClient.getQueryData(["post", postId]);

    queryClient.setQueryData(["post", postId], (old) => ({
      ...old,
      likes: old.likes + 1,
    }));

    return { previous };
  },
  onError: (_err, postId, context) => {
    queryClient.setQueryData(["post", postId], context.previous);
  },
  onSettled: (_data, _err, postId) => {
    queryClient.invalidateQueries({ queryKey: ["post", postId] });
  },
});

onMutate applies the optimistic change and snapshots the previous value, onError rolls back, and onSettled refetches to reconcile with the server.

Pagination and infinite queries

TanStack Query has first-class support for paginated and infinite data. useQuery with a keepPreviousData option makes page transitions smooth, and useInfiniteQuery handles cursor-based lists with a fetchNextPage function. Because each page is part of the cache, back and forward navigation is instant.

Server state versus client state

This is the distinction that makes TanStack Query click:

  • Server state is owned by the server, shared, asynchronous and can go stale. Use TanStack Query.
  • Client state is owned by the browser, synchronous and local. Use useState, Context, Zustand or Redux.

Mixing the two in one store is the source of a lot of complexity. Splitting them — TanStack Query for data, a small store for UI state — keeps each simple.

Best practices

  • Include every variable in the query key.
  • Set staleTime deliberately; the default of zero refetches aggressively.
  • Use mutations plus invalidation rather than hand-updating the cache everywhere.
  • Keep query functions small and colocated with the query.
  • Add optimistic updates only where instant feedback matters.
  • Use the devtools to inspect cache state while developing.
  • Keep server state in TanStack Query and client state elsewhere.

Common mistakes

  • Using the same key for different inputs and serving stale or wrong data.
  • Leaving staleTime at zero and triggering constant refetches.
  • Duplicating server data into a global store.
  • Forgetting to invalidate after a mutation and showing outdated data.
  • Writing optimistic updates without a rollback path.
  • Treating query functions as components and calling hooks inside them.

Where to go next

TanStack Query is the standard way to handle server data in modern apps. Pair it with the Fetch API for the request layer, a client store like Zustand for UI state, and Redux Toolkit if you need strict global state. For Vue, Pinia handles client state while TanStack Query covers the server side.

Fetching data

useQuery caches, deduplicates and exposes state. The effect version re-fetches on every mount and needs manual flags.

Prefer
const { data, isLoading, error } = useQuery({
  queryKey: ["posts"],
  queryFn: fetchPosts,
});
Avoid
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
  fetch("/api/posts")
    .then((r) => r.json())
    .then(setData)
    .finally(() => setLoading(false));
}, []);

Keeping data fresh

Invalidate the query by key after a mutation so every consumer refetches. No manual cache surgery.

Prefer
const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: addPost,
  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: ["posts"],
    });
  },
});
Avoid
// refetching by hand in
// every component that
// happens to show posts

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning TanStack Query?

Our interactive tutorial walks you through TanStack Query step by step — with quizzes and real code you can run in the browser.