What does React Query (TanStack Query) do?
Short answer:
React Query (TanStack Query) is a "smart server-data manager" for React, which takes care of everything: loading, cache, refetch, errors, synchronization, optimistic updates, etc.
It makes working with an API reactive, fast, and safe, without manual
useEffectanduseState.
What React Query (TanStack Query) does
React Query is a "data client", which manages loading, caching, updating, and synchronizing data in React apps.
That is:
- you describe how to fetch the data (queryFn) and what this data is (queryKey),
- and React Query itself:
- makes the request;
- caches the result;
- updates the UI when the data changes;
- revalidates the cache;
- manages "loading", "error", and "stale" states;
- syncs everything on window focus, tab switching, etc.
In short: a "Server state manager"
React Query doesn't replace Redux / Zustand, because it's not for client state (UI state), but for server state - data fetched from an API.
| Type of state | Example | Where to store it |
|---|---|---|
| Client state | modal open/closed, selected tab | useState / Redux / Zustand |
| Server state | users, posts, orders, products | React Query |
Example of basic usage
import { useQuery } from "@tanstack/react-query";
function UserProfile({ id }) {
const { data, isLoading, error } = useQuery({
queryKey: ['user', id],
queryFn: () => fetch(`/api/users/${id}`).then(res => res.json()),
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error loading user</p>;
return <p>{data.name}</p>;
}React Query:
- makes
fetch()once; - caches the result under the key
['user', id]; - if the component unmounts and appears again, the data is already in the cache, no new request needed;
- if focus returns to the window, it automatically refreshes the data.
Main features
1. Data caching
React Query caches data in memory (QueryClient) with flexible settings:
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>Options:
useQuery({
staleTime: 5 * 60 * 1000, // considered fresh for 5 minutes
cacheTime: 10 * 60 * 1000, // removed from memory after 10 min
});2. Stale-While-Revalidate
Shows cached (stale) data instantly, while making a request for fresh data in parallel and updating the UI afterward.
-> A UX with no loader "flicker".
3. Refetch on focus / reconnect
If a user minimized a tab and then came back:
- React Query automatically refetches data (if it's "stale").
<QueryClientProvider client={new QueryClient({
defaultOptions: { queries: { refetchOnWindowFocus: true } }
})}>4. Deduplication
If two components call the same query:
useQuery({ queryKey: ['user', 1], queryFn: fetchUser });
useQuery({ queryKey: ['user', 1], queryFn: fetchUser });React Query executes only one request, and both components receive the same data from the cache.
5. Mutations (POST / PUT / PATCH / DELETE)
For changing data there's useMutation():
import { useMutation, useQueryClient } from '@tanstack/react-query';
function UpdateUserButton({ id }) {
const queryClient = useQueryClient();
const updateUser = useMutation({
mutationFn: (data) => fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}),
onSuccess: () => {
// After a successful update, invalidate the cache
queryClient.invalidateQueries(['user', id]);
},
});
return (
<button onClick={() => updateUser.mutate({ name: 'Alex' })}>
Update
</button>
);
}6. Optimistic updates
Instantly updates the UI before the server responds, and rolls back on an error.
const mutation = useMutation({
mutationFn: likePost,
onMutate: async (postId) => {
await queryClient.cancelQueries(['post', postId]);
const prev = queryClient.getQueryData(['post', postId]);
queryClient.setQueryData(['post', postId], old => ({
...old, likes: old.likes + 1
}));
return { prev };
},
onError: (_, postId, ctx) => {
queryClient.setQueryData(['post', postId], ctx.prev);
},
onSettled: (postId) => {
queryClient.invalidateQueries(['post', postId]);
},
});7. Pagination, Infinite scroll, Prefetch
Built-in support for pagination and loading pages:
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetch(`/api/posts?page=${pageParam}`).then(r => r.json()),
getNextPageParam: (lastPage) => lastPage.nextPage,
});8. Cache persistence
You can persist the cache across sessions:
import { persistQueryClient } from '@tanstack/react-query-persist-client';
persistQueryClient({
queryClient,
persister: createSyncStoragePersister({ storage: window.localStorage }),
});-> data isn't lost on reload, requests aren't repeated.
What this looks like conceptually
React Query takes care of:
manual fetch()
useEffect()
useState()
loading / error / success state
caching and invalidation
repeated requests on focus
all automaticallyKey concepts
| Term | What it is |
|---|---|
| Query | Reading data (GET) |
| Mutation | Changing data (POST, PUT, DELETE) |
| Query Key | A unique identifier for a query in the cache |
| Query Function | The function that performs the request |
| Query Client | The central cache manager |
| Invalidation | Updating the cache after changes |
| Stale Time | How long the data is considered fresh |
| Cache Time | How long before the data is removed from memory |
Summary
| Capability | What React Query does |
|---|---|
| Automatic requests | makes and repeats fetches |
| Caching | stores data by key |
| Data refresh | refetch on focus/change |
| State management | isLoading, isError, isFetching, data |
| Optimistic updates | instant UI feedback |
| Cache invalidation | refreshes after a mutation |
| Deduplication | one request, many components |
| Cache persistence | across tabs/sessions |
| Infinite Scroll / Pagination | ready-made hooks |
| No extra boilerplate | no useEffect/useState for requests |
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.