Summary

TanStack Query (formerly known as React Query) is a powerful asynchronous state management and data-fetching library for JavaScript applications. It’s part of the broader TanStack ecosystem (which also includes TanStack Table, Router, Form, etc.).

At its core, TanStack Query handles the server-state problem: fetching, caching, synchronizing, and updating remote data in your UI. Instead of manually managing loading states, error states, and cache invalidation yourself, TanStack Query handles all of that for you.

Key Hooks

  • useQuery - very useful hook for fetching/reading data. You give it a key and a fetch function and it will manage the rest for you (loading, error, stale data, background refetching)
  • useMutation - for creating/updating/deleting data. Gives you callbacks like onSuccess and onError.

Key Use-Cases

  • Query Keys - a unique identifier for each query, used for caching and invalidation. e.g. ['user', userId].
  • Stale-While-Revalidate - serves cached data immediately while fetching fresh data in the background.
  • Query Invalidation - after a mutation, you can invalidate related queries so they automatically refetch.

Use-Cases

Without it, you’d be writing a lot of useEffect + useState boilerplate for every API call, manually handling race conditions, cache expiry, retry logic, and refetching on window focus. TanStack Query gives you all of that out of the box.

Fetching Data

Plain React

const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
 
useEffect(() => {
  setIsLoading(true);
  fetch('/api/todos')
    .then(res => res.json())
    .then(setData)
    .catch(setError)
    .finally(() => setIsLoading(false));
}, []);

TanStack Query — useQuery

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

Fetching with a Dependency (e.g. by ID)

Plain React

useEffect(() => {
  if (!userId) return;
  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(setData);
}, [userId]);

TanStack Query — useQuery

const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
  enabled: !!userId, // won't run until userId exists
});

Creating / Updating / Deleting

Plain React

const [isPending, setIsPending] = useState(false);
 
const handleSubmit = async () => {
  setIsPending(true);
  try {
    await fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) });
    // manually refetch or update state...
  } finally {
    setIsPending(false);
  }
};

TanStack Query — useMutation

const queryClient = useQueryClient();
 
const mutation = useMutation({
  mutationFn: (newTodo) => fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) }),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] }); // auto-refetch
  },
});
 
mutation.mutate(newTodo);

Multiple Queries in Parallel

Plain React

useEffect(() => {
  Promise.all([fetch('/api/users'), fetch('/api/posts')])
    .then(([u, p]) => Promise.all([u.json(), p.json()]))
    .then(([users, posts]) => { setUsers(users); setPosts(posts); });
}, []);

TanStack Query — useQueries

const [usersQuery, postsQuery] = useQueries({
  queries: [
    { queryKey: ['users'], queryFn: () => fetch('/api/users').then(r => r.json()) },
    { queryKey: ['posts'], queryFn: () => fetch('/api/posts').then(r => r.json()) },
  ],
});

Infinite / Paginated Data

Plain React

const [pages, setPages] = useState([]);
const [page, setPage] = useState(1);
 
useEffect(() => {
  fetch(`/api/posts?page=${page}`)
    .then(res => res.json())
    .then(data => setPages(prev => [...prev, data]));
}, [page]);

TanStack Query — useInfiniteQuery

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam = 1 }) => fetch(`/api/posts?page=${pageParam}`).then(r => r.json()),
  getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
});
 
// data.pages is an array of all fetched pages

Accessing Cached Data Elsewhere

Plain React — you’d need to lift state up, use Context, or a global store (Zustand, Redux, etc.)

TanStack Query — useQueryClient

const queryClient = useQueryClient();
 
// Read from cache
const todos = queryClient.getQueryData(['todos']);
 
// Manually set cache
queryClient.setQueryData(['todos'], newTodos);
 
// Invalidate (trigger refetch)
queryClient.invalidateQueries({ queryKey: ['todos'] });

Summary Table

Use CasePlain ReactTanStack Query
Fetch datauseEffect + useStateuseQuery
Fetch with dependencyuseEffect with conditionuseQuery + enabled
Create/update/deletemanual fetch + stateuseMutation
Parallel queriesPromise.all in useEffectuseQueries
Paginated/infinite datamanual page stateuseInfiniteQuery
Access cache globallyContext / prop drillinguseQueryClient

The pattern that jumps out most is that TanStack Query essentially replaces most of your useEffect + useState combos for anything server-related, and gives you caching, deduplication, background refetching, and retry logic for free on top of that.

What’s the Industry Standard?

It depends on the stack, but the rough consensus is:

ContextStandard
REST API, modern React appTanStack Query
Next.js/ Vercel ecosystemSWR (but TanStack Query works fine too)
Redux-heavy codebaseRTK Query
GraphQL APIApollo Client
Full-stack TypeScript (Next.js, etc.)tRPC + TanStack Query

TanStack Query is arguably the most widely adopted for general React + REST setups. It consistently tops npm download charts and is the default recommendation in most modern React tutorials, job postings, and open source projects. SWR is a close second but has fewer features.


Linked Map of Contexts