Sitemap
Press enter or click to view image in full size

Modern Async with Redux Toolkit Query (RTK Query) — Complete Guide

--

RTK Query is the modern way of handling APIs in Redux.

Before RTK Query, developers usually used:

  • redux-thunk
  • axios
  • manual loading states
  • manual caching
  • manual reducers

This created:

  • lots of boilerplate
  • repeated code
  • hard-to-manage caching

So Redux Toolkit introduced RTK Query.

It automatically handles:

  • API requests
  • loading states
  • error states
  • caching
  • refetching
  • deduplication
  • invalidation
  • polling

What is RTK Query?

RTK Query is a powerful data fetching and caching tool built into:

It helps you:

  • fetch data
  • cache data
  • update server data
  • sync UI with backend

without writing huge async thunk boilerplate.

Traditional Redux Async Flow vs RTK Query

OLD WAY (Thunk)

You had to create:

  • actions
  • reducers
  • loading states
  • error states
  • async functions
  • useEffect logic

Example:

dispatch(fetchUsers())

Then manually manage:

isLoading
error
data

RTK Query Way

You only define endpoints:

getUsers: builder.query({
query: () => '/users'
})

And RTK Query automatically gives:

const { data, error, isLoading } = useGetUsersQuery()

Everything handled automatically.

Core Concepts of RTK Query

RTK Query has these major concepts:

  1. createApi
  2. reducerPath
  3. baseQuery
  4. endpoints
  5. queries
  6. mutations
  7. tags
  8. middleware
  9. listeners
  10. caching system

Let’s understand ALL deeply.

1. createApi()

This is the heart of RTK Query.

It creates:

  • reducers
  • middleware
  • hooks
  • caching system

Example:

export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com',
}),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
}),
}),
});

2. reducerPath (VERY IMPORTANT)

What is reducerPath?

reducerPath is the key name where RTK Query stores API cache inside Redux store.

Example:

reducerPath: 'api'

Redux store becomes:

{
api: {
queries: {},
mutations: {},
provided: {},
subscriptions: {}
}
}

So:

  • api is the slice name
  • all RTK Query cache lives there

Why reducerPath exists?

Because Redux store needs a unique location for API state.

Example:

reducerPath: 'usersApi'

Store:

{
usersApi: { ... }
}

Can we have multiple APIs?

YES.

Example:

reducerPath: 'authApi'
reducerPath: 'productsApi'

Each stores separate cache.

3. baseQuery (VERY IMPORTANT)

3. baseQuery (VERY IMPORTANT)

What is baseQuery?

baseQuery is the function responsible for making network requests.

Think of it as:

“How should RTK Query communicate with server?”

fetchBaseQuery()

Redux Toolkit provides built-in:

fetchBaseQuery()

It is a lightweight wrapper around:

fetch()

Example:

baseQuery: fetchBaseQuery({
baseUrl: 'https://api.example.com',
})

Now every endpoint automatically uses this base URL.

How URLs combine

Example:

baseUrl: 'https://api.example.com'

Endpoint:

query: () => '/users'

Final URL:

https://api.example.com/users

Adding Headers

Example:

baseQuery: fetchBaseQuery({
baseUrl: 'https://api.example.com',
prepareHeaders: (headers) => {
headers.set('Authorization', 'Bearer TOKEN')
return headers
}
})

Used for:

  • JWT token
  • auth
  • custom headers

Custom Base Query

You can also use Axios.

const axiosBaseQuery =
({ baseUrl }) =>
async ({ url, method, data }) => {
try {
const result = await axios({
url: baseUrl + url,
method,
data,
})

return { data: result.data }
} catch (axiosError) {
return {
error: axiosError.response.data,
}
}
}

4. Endpoints

Endpoints define API operations.

Example:

endpoints: (builder) => ({
getUsers: builder.query({...}),
addUser: builder.mutation({...}),
})

5. Queries

Queries are used for:

  • GET requests
  • fetching data

Example:

getUsers: builder.query({
query: () => '/users',
})

Generated hook:

useGetUsersQuery()

Usage:

const { data, isLoading, error } = useGetUsersQuery()

Query Lifecycle

When component mounts:

RTK Query:

  1. checks cache
  2. if no cache → request sent
  3. stores result in Redux
  4. shares cached data with all components

Auto Caching

If 5 components call:

useGetUsersQuery()

ONLY ONE network request happens.

This is HUGE optimization.

6. Mutations

Mutations are used for:

  • POST
  • PUT
  • PATCH
  • DELETE

Example:

addUser: builder.mutation({
query: (user) => ({
url: '/users',
method: 'POST',
body: user,
}),
})
useAddUserMutation()
const [addUser, result] = useAddUserMutation()

addUser({
name: 'Wali'
})

Query Example

const { data } = useGetUsersQuery()

Mutation Example

const [createUser] = useCreateUserMutation()

createUser(userData)

Runs manually.

7. Tags (VERY IMPORTANT)

Tags solve one huge problem:

“How to refresh old cached data automatically?”

Example Problem

Suppose:

  1. You fetched users
  2. Added new user
  3. UI still shows old cached users

Why?
Because cache not refreshed.

Tags Fix This

RTK Query uses:

  • providesTags
  • invalidatesTags

providesTags

Query provides tags.

Get Wali Muhammad’s stories in your inbox

Join Medium for free to get updates from this writer.

Example:

getUsers: builder.query({
query: () => '/users',
providesTags: ['Users'],
})

Meaning:

“This query belongs to Users cache.”

invalidatesTags

Mutation invalidates cache.

Example:

addUser: builder.mutation({
query: (user) => ({
url: '/users',
method: 'POST',
body: user,
}),
invalidatesTags: ['Users'],
})

Meaning:

“After adding user, Users cache becomes outdated.”

RTK Query automatically:

  1. refetches users
  2. updates UI

WITHOUT manual dispatch.

Real Flow

Step 1

Fetch users:

useGetUsersQuery()

Cache stored.

Step 2

Add user:

addUser()

Step 3

invalidatesTags: ['Users']

RTK Query:

  • detects stale cache
  • auto refetches users

Amazing automation.

8. Reducers

What are reducers?

Reducers update Redux state.

They are pure functions.

Example:

(state, action) => {
state.count += 1
}

In RTK Query

RTK Query internally creates reducers automatically.

These reducers store:

  • API cache
  • loading states
  • errors
  • subscriptions

You don’t manually write them usually.

9. Middleware (EXTREMELY IMPORTANT)

What is middleware?

Middleware sits between:

dispatch(action)
↓
middleware
↓
reducer

It intercepts actions.

Why RTK Query needs middleware?

Because RTK Query handles:

  • caching
  • polling
  • refetching
  • subscriptions
  • garbage collection

Middleware manages all this.

Adding Middleware

middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)

Without this:

  • RTK Query will NOT work correctly.

10. Listeners

Listeners enable smart behaviors.

Example:

  • refetch on reconnect
  • refetch on focus

Setup Listeners

setupListeners(store.dispatch)

Why use listeners?

Suppose internet disconnects.

When internet reconnects:

  • RTK Query auto refetches data.

Or:

User leaves tab and returns.

RTK Query can refresh stale data.

Very useful.

What is fetchBaseQuery in RTK Query?

fetchBaseQuery is a small utility function provided by:

Redux Toolkit

It is used inside RTK Query to make API/network requests.

Think of it like this:

RTK Query needs someone to actually call the API.
fetchBaseQuery does that job.

Simple Beginner Analogy

Imagine you own a restaurant app.

Your React components say:

"I need users data"
"I need products data"
"I need to add a product"

But someone still has to:

  • send request to server
  • attach headers
  • handle response
  • convert JSON

That “someone” is:

fetchBaseQuery

The Problem Before fetchBaseQuery

Before RTK Query, developers manually wrote:

fetch('https://api.com/users')

or

axios.get('/users')

again and again.

For every request we repeated:

  • base URL
  • headers
  • token
  • JSON conversion
  • error handling

This became repetitive.

Why fetchBaseQuery Exists

Redux Toolkit created fetchBaseQuery to:

✅ reduce boilerplate
✅ simplify API setup
✅ standardize requests
✅ work perfectly with RTK Query
✅ automatically handle JSON

What Does fetchBaseQuery Actually Do?

Internally it uses browser’s:

fetch()

API.

But it adds:

  • easier syntax
  • automatic JSON parsing
  • integration with RTK Query
  • cleaner setup

Basic Syntax

fetchBaseQuery({
baseUrl: 'https://api.example.com'
})

Full Example

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const api = createApi({
reducerPath: 'api',

baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com',
}),

endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
}),
}),
})

Understanding Flow Step-by-Step

Step 1 — baseUrl

baseUrl: 'https://jsonplaceholder.typicode.com'

This becomes the main URL for ALL requests.

Step 2 — Endpoint Adds Route

query: () => '/users'

RTK Query combines them:

https://jsonplaceholder.typicode.com/users

Step 3 — fetchBaseQuery Sends Request

Internally:

fetch('https://jsonplaceholder.typicode.com/users')

Step 4 — Response Converted to JSON

Normally with fetch you write:

const response = await fetch(url)
const data = await response.json()

But fetchBaseQuery does this automatically.

Huge convenience.

Visual Mental Model

Component
↓
useGetUsersQuery()
↓
Endpoint query()
↓
fetchBaseQuery()
↓
fetch()
↓
Server
↓
JSON response
↓
RTK Query cache
↓
Component updates

What Happens WITHOUT fetchBaseQuery?

You would need custom logic manually.

Example:

const customBaseQuery = async ({ url }) => {
const response = await fetch(url)

const data = await response.json()

return { data }
}

Redux Toolkit simplified this by giving:

fetchBaseQuery()

ready-made.

Deep Understanding of baseQuery

In RTK Query:

baseQuery

means:

“HOW should requests be made?”

You can use:

  • fetchBaseQuery
  • axios custom function
  • graphql function
  • custom request system

But Redux Toolkit recommends:

fetchBaseQuery

How fetchBaseQuery Internally Works

Simplified internal version:

const fetchBaseQuery = ({ baseUrl }) => {
return async ({ url, method }) => {
const response = await fetch(baseUrl + url, {
method,
})

const data = await response.json()

return { data }
}
}

This is not exact source code but helps understanding.

Using GET Request

Example:

getUsers: builder.query({
query: () => '/users',
})

Internally:

fetch('/users')

Using POST Request

Example:

addUser: builder.mutation({
query: (user) => ({
url: '/users',
method: 'POST',
body: user,
}),
})
fetch('/users', {
method: 'POST',
body: JSON.stringify(user)
})

Why body Works Automatically

fetchBaseQuery automatically:

  • converts body to JSON
  • sets content-type header

Without manual work.

--

--

Wali Muhammad
Wali Muhammad

Written by Wali Muhammad

Consultant Frontend Developer