Skip to content

israrahmadtech/Tanstack-Query-Guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 

Repository files navigation

TanStack Query (React Query) - Beginner Friendly Guide 🚀

A beginner-friendly explanation of the core concepts of TanStack Query, written in a storytelling style. If you've ever wondered "How does TanStack Query actually work behind the scenes?", this guide is for you.


Table of Contents

  • What is TanStack Query?
  • Client State vs Server State
  • QueryClient
  • QueryClientProvider
  • useQuery
  • queryKey
  • queryFn
  • cache
  • fresh vs stale
  • refetch
  • useMutation
  • mutationFn
  • mutate()
  • onSuccess
  • useQueryClient
  • invalidateQueries()
  • Query Flow
  • Mutation Flow
  • Common Query States
  • Summary Table

What is TanStack Query?

Imagine you have a backend API.

GET /todos

Whenever your React application needs todos, it has to call this API.

Without TanStack Query, every developer usually writes:

  • loading state
  • error state
  • fetch logic
  • refetch logic
  • caching logic

again and again.

TanStack Query says:

"Don't worry, I'll manage all of this for you."

Its main job is to manage Server State.


Client State vs Server State

Before learning TanStack Query, you must understand these two terms.

Client State

This is the data that only exists inside your React application.

Examples:

  • Dark Mode
  • Sidebar Open/Close
  • Selected Tab
  • Counter Value
const [theme, setTheme] = useState("dark");
const [count, setCount] = useState(0);

React itself is enough to manage this.


Server State

This is the data stored somewhere else.

Usually inside:

  • Database
  • Backend
  • API

Example:

GET /users

↓

Returns

↓

[
  {
    "id":1,
    "name":"Israr"
  }
]

This data belongs to the server.

TanStack Query manages this data.


QueryClient

Think of QueryClient as the Brain of TanStack Query.

const queryClient = new QueryClient();

The moment you create it, TanStack Query gets a place where it can store everything.

It keeps track of things like:

  • Cached Data
  • Query Status
  • Mutation Status
  • Retry Information
  • Loading States
  • Error States

Think of it like this:

QueryClient

├── Todos Cache
├── Users Cache
├── Posts Cache
├── Loading Status
├── Error Status
└── Mutation Status

Every query and mutation eventually communicates with this client.


QueryClientProvider

Creating a QueryClient isn't enough.

Now the whole application needs access to it.

That's where QueryClientProvider comes in.

<QueryClientProvider client={queryClient}>
    <App />
</QueryClientProvider>

Think of it like Wi-Fi.

You bought a Wi-Fi router.

Great.

But unless you turn it on and let every device connect to it, nobody can use the internet.

Similarly,

QueryClient = Wi-Fi Router

QueryClientProvider = Sharing the Wi-Fi with the whole app

Without this Provider,

every useQuery() or useMutation() will throw an error because they won't know where the QueryClient is.


useQuery

This is the most commonly used hook.

const query = useQuery({
    queryKey:["todos"],
    queryFn:getTodos
});

The moment this runs, TanStack Query starts a story.

Component Rendered

↓

Do we already have cached data?

↓

YES

↓

Return Cached Data

OR

↓

NO

↓

Call queryFn()

↓

Receive Data

↓

Save it into Cache

↓

Render UI

The developer doesn't have to manually manage loading, caching, or refetching.

TanStack Query handles everything.


queryKey

Every query must have an identity.

That's exactly what queryKey is.

queryKey:["todos"]

Imagine a library.

Every book has a unique number.

Without that number, the librarian cannot find the correct book.

Similarly,

TanStack Query stores data using keys.

Example:

Cache

↓

todos

↓

[
  ...
]

Another query:

queryKey:["users"]

Now cache becomes:

Cache

├── todos
└── users

Each key points to different cached data.


queryFn

This is simply the function responsible for fetching data.

queryFn:getTodos

Example:

async function getTodos(){

    const response = await fetch("/api/todos");

    return response.json();

}

TanStack Query doesn't know how your backend works.

It simply calls this function whenever data is needed.


cache

One of the biggest reasons developers love TanStack Query.

Imagine this.

Without cache:

Open Page

↓

GET Request

↓

Go Back

↓

Open Again

↓

Another GET Request

↓

Open Again

↓

Another GET Request

Your API keeps getting called again and again.

Now imagine using cache.

Open Page

↓

GET Request

↓

Store Data

↓

Open Again

↓

Read from Cache

↓

Instant UI

Much faster.

Much fewer API calls.

Better user experience.


Fresh vs Stale

Every cached data has a condition.

Either it's:

  • Fresh
  • Stale

Fresh

Fresh means:

This data is still trusted.

No need to call the API.

TanStack Query happily uses the cache.


Stale

Stale means:

This data may be outdated.

Now TanStack Query knows:

Next opportunity I get,

I'll fetch the latest data.

Stale does NOT mean deleted.

It simply means:

"This data should probably be refreshed."


refetch

Sometimes you don't want to wait.

You want the newest data immediately.

That's called refetching.

query.refetch();

Flow:

Current Data

↓

Call API Again

↓

Receive Latest Data

↓

Replace Cache

↓

Update UI

useMutation

Queries read data.

Mutations change data.

Whenever you:

  • POST
  • PUT
  • PATCH
  • DELETE

you'll usually use:

useMutation()

Example:

const mutation = useMutation({
    mutationFn:postTodo
});

mutationFn

Just like queryFn fetches data,

mutationFn changes data.

Example:

async function postTodo(todo){

    await fetch("/todos",{
        method:"POST",
        body:JSON.stringify(todo)
    });

}

This function is responsible for sending data to the server.


mutate()

Nothing happens until you execute the mutation.

mutation.mutate({
    title:"Learn Next.js"
});

Story:

User Clicks Button

↓

mutate()

↓

mutationFn()

↓

POST Request

↓

Database Updated

Notice something important.

The database changed.

But...

The cache did NOT.

This is where many beginners get confused.


onSuccess

After the mutation finishes successfully,

TanStack Query calls:

onSuccess:()=>{}

This is simply a callback.

Think of it as:

POST Finished Successfully

↓

Now execute this code.

This is where developers usually update or refresh cached queries.


useQueryClient

Sometimes you need direct access to the QueryClient.

That's exactly what this hook provides.

const queryClient = useQueryClient();

Now you can:

  • invalidate queries
  • manually update cache
  • refetch queries
  • inspect cached data

Without this hook,

you cannot communicate directly with the QueryClient.


invalidateQueries()

This is probably the most misunderstood concept.

Let's understand it through a story.


Imagine your database currently contains:

Learn React

Learn Node

Your page loads.

useQuery() fetches the data.

Now cache becomes:

Cache

↓

Learn React

Learn Node

Everything looks perfect.


Now the user clicks:

Add Todo

A mutation runs.

POST /todos

Database becomes:

Learn React

Learn Node

Learn Next.js

But...

The cache still contains:

Learn React

Learn Node

Because mutations DO NOT automatically update queries.

TanStack Query has no idea which query should refresh.

Should it refresh:

  • todos?
  • users?
  • dashboard?
  • statistics?

It cannot guess.

So you tell it manually.

onSuccess:()=>{

    queryClient.invalidateQueries({
        queryKey:["todos"]
    });

}

Now the story becomes:

Mutation Success

↓

Invalidate "todos"

↓

Mark Cache as Stale

↓

Automatically Refetch

↓

Receive Latest Data

↓

Replace Cache

↓

UI Updated

Notice something very important.

invalidateQueries() does NOT update data.

It only says:

This cache is now stale.

Please fetch it again.

That's all.

The actual updated data comes from a new GET request, not from invalidateQueries() itself.


Query Flow

Component Mounted

↓

useQuery()

↓

Check Cache

↓

If Data Exists

↓

Return Cache

↓

Otherwise

↓

Run queryFn()

↓

Receive Data

↓

Save Into Cache

↓

Render UI

Mutation Flow

User Clicks Button

↓

mutate()

↓

mutationFn()

↓

POST Request

↓

Database Updated

↓

onSuccess()

↓

invalidateQueries()

↓

Cache Marked Stale

↓

GET Request Again

↓

Latest Data Received

↓

Cache Updated

↓

UI Updated

Common Query States

When using useQuery(), you'll frequently encounter these properties.

data

The actual response returned from your API.

query.data

isLoading

The very first request is still loading.

No data yet.

Waiting for server.

isFetching

A request is currently happening.

This includes background refetches.

Unlike isLoading, this isn't limited to the first request.


isError

Something went wrong.

The request failed.


isSuccess

Everything completed successfully.

The data is available.


Summary Table

Keyword Meaning
QueryClient The brain of TanStack Query. Stores caches, queries, mutations, and manages everything internally.
QueryClientProvider Makes the QueryClient available throughout your React application.
useQuery Fetches server data and automatically caches it.
useMutation Performs POST, PUT, PATCH, and DELETE requests.
useQueryClient Gives direct access to the QueryClient instance.
queryKey A unique identifier used to store and retrieve cached data.
queryFn The function responsible for fetching data from the server.
mutationFn The function responsible for changing data on the server.
mutate() Executes the mutation.
onSuccess Runs after a successful mutation.
invalidateQueries() Marks specific cached queries as stale and triggers a refetch.
cache Temporary storage for fetched server data.
fresh Cached data is still trusted.
stale Cached data may be outdated and should be refreshed.
refetch Requests the latest data from the server again.
isLoading Indicates the first request is loading.
isFetching Indicates any request is currently running, including background refetches.
isError Indicates the request failed.
isSuccess Indicates the request completed successfully.
data The data returned from the server.

Final Thoughts

TanStack Query is not just a data fetching library.

It is a Server State Management Library.

Instead of worrying about:

  • Loading
  • Errors
  • Refetching
  • Caching
  • Synchronizing server data

you simply describe what data you need, and TanStack Query takes care of the rest.

Once these core concepts become clear, learning advanced features like pagination, infinite queries, optimistic updates, prefetching, and cache manipulation becomes much easier.

Happy Coding! 🚀

About

A beginner-friendly explanation of the core concepts of **TanStack Query**, written in a storytelling style. If you've ever wondered *"How does TanStack Query actually work behind the scenes?"*, this guide is for you.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors