tanstack query simplified
Here I have compiled my understanding of React Query (TanStack Query) over the months and simplified it into my personal study reference. This is mainly written for myself, but I believe it can also serve as a good starting guide for someone who is new to learning TanStack Query.
React Query (TanStack Query)
Here I have compiled my understanding of React Query over the months and simplified it into my personal study reference.
This should also prove to be a good guide for someone who is new to learning React Query.
What is React Query?
React Query, now called TanStack Query, is a client-side data-fetching and server-state management library.
But it comes with lots of abilities.
It gives us automatic caching, background refetching, loading and error states, window-focus refetching, query deduplication, retries, and cache garbage collection.
Instead of manually writing all the logic for handling data that comes from an API, React Query gives us tools to manage that server data.
For example, without React Query, I may have to manually handle:
- Loading state
- Error state
- API response
- Caching
- Refetching
- Retry logic
- Keeping data fresh
React Query helps me handle these things for me.
Installation
First, install React Query:
npm install @tanstack/react-query
If I also want the React Query Devtools:
npm install @tanstack/react-query-devtools
Or both at once:
npm install @tanstack/react-query @tanstack/react-query-devtools
Creating the QueryClient
After installing React Query, we need to create a QueryClient.
The QueryClient is the central "brain and memory storage" of React Query.
It maintains the query cache, tracks query states, manages inactive cached data, and applies default configurations.
I can think about it like this:
The
QueryClientis the place where React Query keeps track of everything it knows about my server data.
In a Next.js application, I can create a providers.tsx file:
app/
├── layout.tsx
└── providers.tsx
Inside providers.tsx:
"use client";
import { useState } from "react";
import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
export function Providers({
children,
}: {
children: React.ReactNode;
}) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5,
refetchOnWindowFocus: true,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
Then I go to layout.tsx and wrap my application with the provider:
import { Providers } from "./providers";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
This setup gives my application a shared, in-memory cache where React Query can store the data returned from my API requests.
This means that components do not always have to make a new network request when the data is already available in the cache.
Fetching Data With useQuery
For fetching data, we use the useQuery hook.
For mutating data, we use the useMutation hook.
A simple way I remember this is:
useQuery
↓
Read/fetch server data
useMutation
↓
Create/update/delete server data
The useQuery Hook
A basic useQuery looks like this:
const {
data,
isPending,
isFetching,
isError,
error,
status,
fetchStatus,
refetch,
} = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
The two most important properties to understand first are queryKey and queryFn.
queryKey
queryKey: ["todos"]
The queryKey is an array that uniquely identifies a query.
I like to think of it as the address of the data in the cache.
For example:
queryKey: ["todos"]
I can mentally imagine something like:
cache["todos"]
This is only a mental model, not how the cache is literally implemented.
React Query uses the query key to determine which cached data belongs to which query.
Using variables in queryKey
The query key can contain variables.
For example:
const { data } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
});
If:
userId = 1
the query key becomes:
["user", 1]
If:
userId = 2
the query key becomes:
["user", 2]
React Query treats these as different queries and caches their results separately.
This is why I think of the query key somewhat like a dependency array.
When a value used in the query key changes, React Query can identify that as a different query and fetch the data associated with that new key.
queryFn
The queryFn is the function responsible for fetching the data.
For example:
async function fetchTodos() {
const response = await fetch("/api/todos");
if (!response.ok) {
throw new Error("Failed to fetch todos");
}
return response.json();
}
Then:
const { data } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
So I remember it like this:
queryKeytells React Query what data I am talking about.
queryFntells React Query how to get that data.
data
const { data } = useQuery(...);
data contains the value returned by the queryFn after the promise successfully resolves.
For example, if my API returns:
[
{
"id": 1,
"title": "Learn React Query"
},
{
"id": 2,
"title": "Build a project"
}
]
then data contains that array.
status
status answers the question:
What is the current state of my query data?
It has three possible values:
pending
error
success
pending
The query does not currently have successful data.
This commonly happens during the initial fetch.
success
The query successfully has data.
error
The query encountered an error.
For example:
if (status === "pending") {
return <p>Loading...</p>;
}
if (status === "error") {
return <p>Something went wrong.</p>;
}
return <div>{/* render data */}</div>;
isPending
Instead of checking:
status === "pending"
I can use:
isPending
For example:
if (isPending) {
return <p>Loading...</p>;
}
isPending means that the query currently has no successful data.
isLoading
isLoading is slightly different from isPending.
It is true when the query is pending and fetching at the same time.
In simple terms:
isLoading = isPending + isFetching
This distinction becomes useful when I start working with cached data.
For example, I may already have cached data on the screen while React Query is fetching a newer version in the background.
In that situation:
isPending → false
isFetching → true
Therefore, isLoading is not necessarily true.
isFetching
isFetching answers the question:
Is the query function currently running?
During the first request:
No cached data
↓
Fetching starts
↓
isPending = true
isFetching = true
But imagine that I already have cached data:
Cached data exists
↓
Background refetch starts
↓
isPending = false
isFetching = true
This is an important concept.
I can have data displayed on the screen while isFetching is still true.
React Query can show the cached data while it contacts the server to see whether there is a newer version.
fetchStatus
fetchStatus answers a different question:
What is happening with the fetching process?
It can have three values:
fetching
paused
idle
fetching
The query function is currently executing.
paused
The query wants to fetch, but fetching is currently paused. This can happen when the device is offline.
idle
There is currently no fetch happening.
The difference between status and fetchStatus is important.
I remember it this way:
| Property | Question |
| ------------- | ------------------------------------------ |
| status | What is the state of my data? |
| fetchStatus | What is the state of the fetching process? |
For example, I can have:
status: "success"
fetchStatus: "fetching"
This means:
I already have data, but React Query is currently fetching a newer version.
isError
const { isError, error } = useQuery(...);
isError becomes true when the query encounters an error.
For example:
if (isError) {
return <p>{error.message}</p>;
}
The queryFn should throw an error when the request fails.
For example:
async function fetchTodos() {
const response = await fetch("/api/todos");
if (!response.ok) {
throw new Error("Failed to fetch todos");
}
return response.json();
}
refetch
refetch is a function that allows me to manually trigger a query again.
const { data, refetch } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
Then:
<button onClick={() => refetch()}>
Refresh
</button>
I can think of refetch() as telling React Query:
"Please run this query again."
enabled
When enabled is false, React Query will not automatically run the query when the component mounts.
This is useful for conditional queries.
For example:
const { data } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
enabled: !!userId,
});
If userId does not exist:
userId = undefined
↓
enabled = false
↓
Query does not automatically run
When userId becomes available:
userId = 10
↓
enabled = true
↓
Query can run
refetchOnWindowFocus
By default, refetchOnWindowFocus is true.
When the user leaves the browser tab and later comes back, React Query checks the query.
If the query is stale, it can automatically perform a background refetch.
For example:
User opens application
↓
Data is fetched
↓
User changes to another tab
↓
Some time passes
↓
User comes back
↓
React Query checks the query
↓
If stale → background refetch
It has three common options:
true
Refetch stale queries when the window regains focus.
false
Do not refetch when the window regains focus.
"always"
Always refetch when the window regains focus, even if the data is still fresh.
staleTime
staleTime answers the question:
How long should this data remain fresh?
For example:
staleTime: 1000 * 60 * 5
This means:
1000 milliseconds
× 60
× 5
= 5 minutes
So:
Data fetched
↓
Fresh for 5 minutes
↓
Becomes stale
While the data is fresh, React Query generally will not make a background request because of triggers such as mounting or window focus.
What Does "Stale" Mean?
Stale does not mean that React Query has deleted the data.
It simply means:
React Query considers the data old enough that it may need to check the server for a newer version.
The data can still remain in the cache.
So:
Fresh
↓
Stale
↓
Still cached
This distinction is very important.
gcTime
gcTime answers the question:
How long should inactive cached data remain in memory?
Suppose a component is using a query:
Component
↓
Using query
↓
Query is active
Then the component unmounts:
Component unmounts
↓
Query becomes inactive
↓
gcTime timer starts
If the query remains unused until the gcTime expires, React Query can remove it from the cache.
So:
staleTime
↓
How long is the data fresh?
gcTime
↓
How long does inactive cached data remain?
staleTime vs gcTime
For example:
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 30,
The lifecycle can be understood as:
Data fetched
↓
Fresh for 5 minutes
↓
Data becomes stale
↓
Still remains cached
↓
Component unmounts
↓
gcTime timer starts
↓
30 minutes of inactivity
↓
Garbage collected
So I remember:
staleTimecontrols freshness.
gcTimecontrols how long inactive cached data remains.
placeholderData
placeholderData allows us to provide temporary data while a query is fetching.
A useful example is pagination.
Suppose I am currently viewing page 1 and then move to page 2.
Without keeping the previous data, the UI can appear empty while page 2 is loading.
We can use keepPreviousData with placeholderData:
import {
keepPreviousData,
useQuery,
} from "@tanstack/react-query";
const { data, isFetching } = useQuery({
queryKey: ["todos", page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
});
This allows the previous page's data to remain visible while the new page is being fetched.
The important thing to remember is:
placeholderDatais temporary data used while the query is fetching. It is not the new fetched result stored as the query's actual data.
initialData
initialData is different from placeholderData.
initialData provides initial data for the query and is used to seed the query's cache.
For example:
const { data } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
initialData: userFromServer,
});
This can be useful when I already have some data available before the query runs.
So I remember it this way:
placeholderData
↓
Temporary data
initialData
↓
Initial data used to seed the cache
select
select is used for data transformation.
It allows us to transform or filter the query data before using it in the component.
For example:
const { data: names } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
select: (users) =>
users.map((user) => user.name),
});
If the API returns:
[
{
"id": 1,
"name": "John"
},
{
"id": 2,
"name": "Mary"
}
]
then names becomes:
["John", "Mary"]
So I can think of select as:
API data
↓
select()
↓
Transformed data
↓
Component
refetchInterval
refetchInterval automatically refetches a query at a specified interval.
For example:
refetchInterval: 3000
means the query can be refetched every 3 seconds.
Fetch
↓
Wait 3 seconds
↓
Fetch again
↓
Wait 3 seconds
↓
Fetch again
This is useful for polling.
For example:
const { data } = useQuery({
queryKey: ["crypto-price"],
queryFn: fetchCryptoPrice,
refetchInterval: 3000,
});
It can be useful for:
- Dashboards
- Job status
- Frequently changing data
- Monitoring systems
retry
retry controls how many times React Query should retry a failed query.
For example:
retry: 3
Conceptually:
Request
↓
Fails
↓
Retry
↓
Fails
↓
Retry
↓
Fails
↓
Retry
↓
Fails
↓
Error state
The retry behavior can also be customized when we need more control.
A Conditional Search Example
Now let's put some of these concepts together.
Suppose I have a search input:
const [searchTerm, setSearchTerm] = useState("");
const [shouldSearch, setShouldSearch] = useState(false);
Then:
const { data: searchResults } = useQuery({
queryKey: ["search", searchTerm],
queryFn: () => fetchSearchResults(searchTerm),
enabled: shouldSearch && searchTerm.length > 0,
});
Here:
queryKey: ["search", searchTerm]
means that each search term has its own query identity.
For example:
["search", "react"]
["search", "nextjs"]
["search", "python"]
are different query keys.
And:
enabled: shouldSearch && searchTerm.length > 0
means the query should only run when:
shouldSearchistruesearchTermis not empty
So:
shouldSearch = false
↓
Query disabled
But:
shouldSearch = true
searchTerm = "react"
↓
Query enabled
↓
fetchSearchResults("react")
My Simple Way of Remembering useQuery
When I see:
useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
I think:
| Property | Question it answers |
| ---------------------- | -------------------------------------------------- |
| queryKey | What data am I talking about? |
| queryFn | How do I get the data? |
| data | What did the server return? |
| status | What is the state of the query data? |
| fetchStatus | What is happening with the fetch? |
| enabled | Should this query run automatically? |
| staleTime | How long should this data be fresh? |
| gcTime | How long should inactive cached data remain? |
| refetchOnWindowFocus | Should I refetch when the user returns to the tab? |
| refetchInterval | Should I keep polling the server? |
| select | Do I want to transform the returned data? |
| placeholderData | What temporary data should I show while fetching? |
| initialData | Do I already have initial data to seed the cache? |
| retry | How many times should a failed query be retried? |
| refetch | Do I want to manually run the query again? |
The Main Idea
The most important thing I have understood is that React Query is not simply a replacement for fetch().
fetch() mainly gives us a way to make an HTTP request.
React Query goes further.
It helps us manage the server state that comes from those requests.
That includes:
- Fetching
- Caching
- Freshness
- Refetching
- Loading states
- Error states
- Retries
- Garbage collection
So instead of thinking:
"React Query is a better
fetch()."
I think:
"React Query helps me manage data that lives on the server but is being used by my React application."
Next: useMutation
So far, useQuery has mainly been about reading data.
For example:
GET /api/products
GET /api/sales
GET /api/users
When I want to change data on the server, I use useMutation.
For example:
POST
PUT
PATCH
DELETE
This leads to another important part of React Query:
useMutationqueryClientinvalidateQueries()- Updating the cache
- Optimistic updates
- Mutation loading and error states
These are the next concepts I need to understand.
Comments (0)
No comments yet. Be the first to share your thoughts.