Getting Started with TanStack Query


What is TanStack Query?
TanStack Query is a tool that helps our React app talk to APIs (servers) easily and smartly.
Normally, we’d write a bunch of code to:
Ask the server for data
Show a loading spinner
Handle errors
Save the result so it doesn’t keep asking again
TanStack Query does all that heavy lifting for us — automatically!
In the usual way (before tools like this), we’d have to:
Use
useEffect()
to trigger a requestUse
fetch()
to get the dataUse
useState()
to store it
This becomes messy really fast. We have to manually manage loading states, errors, refreshing, etc.
TanStack Query makes all this simpler and cleaner.
Use TanStack Query when:
Our app gets data from a server or API (like a to-do list or user profiles)
We want automatic caching (so we don’t fetch the same data again and again)
We want data to update in the background, stay fresh, and sync with our UI
1.Installation & Setup
Step 1: Install the Tool
First, we need to add TanStack Query to our project*.* Open the terminal and run:
npm add @tanstack/react-query
or
yarn add @tanstack/react-query
Step 2: Set up the Brains – QueryClient
Before using TanStack Query, we need to create a "manager" that keeps track of all our data queries. This is called a QueryClient
.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
Step 3: Wrap Your App with QueryClientProvider
Next, we tell React:
“Hey, we’re using TanStack Query — here’s the brain that manages everything!”
So we wrap your whole app with QueryClientProvider
and pass it the queryClient
we just created:
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
2.Basic Usage – useQuery
Now that we’ve set up TanStack Query, let’s learn how to actually fetch some data in our app.
Imagine we want to get a list of users from an API. TanStack Query gives us a special tool called useQuery
to do just that — easily and smartly.
What is useQuery
?
useQuery
is a React hook — just like useState
or useEffect
— but designed specifically for getting data from a server*.*
It helps us:
Fetch data from an API
Show a loading spinner while waiting
Handle errors if something goes wrong
Store and reuse the data (cache it)
Let’s write a basic function to fetch users from a fake API:
const getUsers = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
if (!res.ok) throw new Error('Failed to fetch users');
return res.json();
};
Now, let’s use the useQuery
hook inside a component:
import { useQuery } from '@tanstack/react-query';
function Users() {
const { data, isLoading, error } = useQuery(['users'], getUsers);
if (isLoading) return <p>Loading users...</p>;
if (error) return <p>Something went wrong: {error.message}</p>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
What’s Happening Here?
useQuery(['users'], getUsers)
:'users'
is a unique key for caching the datagetUsers
is the function we wrote earlier
isLoading
: becomestrue
while data is loadingerror
: holds any error that happens during the requestdata
: holds the list of users once it's ready
3. useQuery
– The Basics
const { data, isLoading } = useQuery(queryKey, queryFn);
React Query’s useQuery
hook takes two main arguments:
→**Query Key (aka queryKey
)
A unique name (usually an array) that identifies the data we're asking for.
Helps React Query cache, track, and refresh that specific data.
We can add variables too:
['user', userId] // a unique key for a specific user
Think of it like a label that says:
"This is the data for ‘users’" or "This is the data for user with ID 5".
→Query Function (aka queryFn
)
const fetchUsers = async () => {
const res = await fetch('/api/users');
if (!res.ok) throw new Error('Error fetching users');
return res.json();
};
This is the function that fetches your data from a server or API.
It should return a promise (async function).
React Query runs this function and manages all the state for us.
→Example: Putting it All Together
const { data, isLoading, isError } = useQuery(['users'], fetchUsers);
'users'
→ Query key (used for caching and invalidation)fetchUsers
→ The function that actually makes the API call
→Why is the key so important?
React Query uses the key to:
Know which data we're asking for
Reuse cached data (no duplicate fetches)
Invalidate or refetch only the specific query when needed
4.Understanding the States in React Query
When we fetch data using useQuery
, React Query gives us back a bunch of helpful state values. These help us show things like loading spinners, error messages, or the actual data — without writing all that logic manually.
State | Meaning |
isLoading | true when the query is loading for the first time |
isFetching | true when the query is fetching in the background (e.g. refetch) |
isError | true if the query failed (e.g. network error) |
error | Holds the error object/message if isError is true |
isSuccess | true when the query succeeded and data is available |
data | Holds the fetched data if the query was successful |
isStale | true if the cached data is outdated and might need refetching |
status | Can be 'loading' , 'error' , or 'success' — same as above states |
5. Using useMutation
While queries are used to get data,
mutations are used to change data — like:
Creating a new item
Updating existing data
Deleting something
import { useMutation } from '@tanstack/react-query';
const addUser = async (newUser) => {
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) throw new Error('Error adding user');
return res.json();
};
const { mutate, isLoading, isError, isSuccess } = useMutation(addUser);
addUser
→ the mutation function (sends data to the server)mutate
→ a function you call to trigger the mutationisLoading
,isError
,isSuccess
→ same helpful state flags like in queries
→Using mutate
to send data
<button onClick={() => mutate({ name: 'John' })}>
Add User
</button>
6. Query Invalidation
Query invalidation means telling React Query:
“Hey, the data you have in cache is outdated — please fetch fresh data!”
Why it’s Important:
When we add, update, or delete something using a mutation, the data in our UI might become stale.
So after a mutation, we invalidate the related query to refresh the data automatically.Real-life Analogy:
Imagine you’re managing a to-do list on your app:
You fetch the list with
useQuery(['todos'], fetchTodos)
Then you add a new task using a mutation
But the UI still shows the old list... unless you invalidate it
That’s where query invalidation comes in!How to Invalidate a Query
We use queryClient.invalidateQueries()
:
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries(['todos']); // Now refetches fresh todos!
},
});
Summary
Feature | What it Does |
queryClient.invalidateQueries(['key']) | Marks that query as stale and refetches it |
Used after mutation | To refresh the list or UI with the latest data |
Keeps UI in sync | Ensures no outdated data stays on screen |
And that’s it — everything we need to get started with TanStack Query!
From fetching data with useQuery
, handling changes with useMutation
, to keeping our UI in sync using query invalidation — we now have the foundation to build fast, reliable, and reactive frontends with ease.
Start small, play around, and soon you’ll be wondering how you ever built React apps without it!
Connect with me on twitter.
Subscribe to my newsletter
Read articles from Priyanshu Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
