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-thunkaxios- 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
dataRTK 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:
- createApi
- reducerPath
- baseQuery
- endpoints
- queries
- mutations
- tags
- middleware
- listeners
- 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:
apiis 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/usersAdding 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:
- checks cache
- if no cache → request sent
- stores result in Redux
- 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:
- You fetched users
- Added new user
- 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:
- refetches users
- 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
↓
reducerIt 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:
fetchBaseQueryThe 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/usersStep 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 updatesWhat 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:
baseQuerymeans:
“HOW should requests be made?”
You can use:
- fetchBaseQuery
- axios custom function
- graphql function
- custom request system
But Redux Toolkit recommends:
fetchBaseQueryHow 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.









