Getting Started with TanStack Query

Priyanshu PatelPriyanshu Patel
6 min read

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 request

  • Use fetch() to get the data

  • Use 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 data

    • getUsers is the function we wrote earlier

  • isLoading: becomes true while data is loading

  • error: holds any error that happens during the request

  • data: 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.

StateMeaning
isLoadingtrue when the query is loading for the first time
isFetchingtrue when the query is fetching in the background (e.g. refetch)
isErrortrue if the query failed (e.g. network error)
errorHolds the error object/message if isError is true
isSuccesstrue when the query succeeded and data is available
dataHolds the fetched data if the query was successful
isStaletrue if the cached data is outdated and might need refetching
statusCan 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 mutation

  • isLoading, 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:

  1. You fetch the list with useQuery(['todos'], fetchTodos)

  2. Then you add a new task using a mutation

  3. 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

FeatureWhat it Does
queryClient.invalidateQueries(['key'])Marks that query as stale and refetches it
Used after mutationTo refresh the list or UI with the latest data
Keeps UI in syncEnsures 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.

0
Subscribe to my newsletter

Read articles from Priyanshu Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Priyanshu Patel
Priyanshu Patel