If you've ever felt intimidated by Redux Toolkit Query (RTK Query), you're not alone. The docs https://redux-toolkit.js.org/tutorials/rtk-query assume you already know what a "reducer" or "middleware" is ā but what if you're brand new to this?
This article explains RTK Query using everyday analogies, so by the end you'll actually understand what's happening instead of just copy-pasting code.
Let's build a tiny app that shows a list of students from a server. š§āš
The Big Picture
Imagine our school website asks a server:
"Can I have the list of students?"
And the server replies:
[
{ "id": 1, "name": "John" },
{ "id": 2, "name": "Mary" }
]
RTK Query is the tool that handles that entire conversation for you ā asking, waiting, and remembering the answer ā so you don't have to write fetch() calls by hand.
Step 1: Project Structure
Here's what our project will look like:
src/
App.jsx
main.jsx
store.js
services/
studentApi.js
Think of it like a school: the School is your app, the Classroom is your store, and the Students are your data. Everything has its own place.
Step 2: Install Redux Toolkit
npm install @reduxjs/toolkit react-redux
This gives you RTK Query for free ā it's built right into @reduxjs/toolkit.
Step 3: Create the API Service
Create src/services/studentApi.js:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const studentApi = createApi({
reducerPath: "studentApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://jsonplaceholder.typicode.com/",
}),
endpoints: (builder) => ({
getStudents: builder.query({
query: () => "users",
}),
}),
});
export const { useGetStudentsQuery } = studentApi;
Let's break down every part of this, piece by piece.
createApi and fetchBaseQuery
Think of building a toy car ā first you need your tools: a hammer, a screwdriver, a spanner. createApi and fetchBaseQuery are your tools for building an API layer.
reducerPath
Think of a cupboard with labelled shelves ā one for books, one for toys, one for shoes. reducerPath: "studentApi" tells Redux: store all the student information on the "studentApi" shelf.
baseQuery
Imagine your friend lives at 12 Green Street. Every time you visit, you start from that same address. baseQuery works the same way ā it's the address every request starts from:
https://jsonplaceholder.typicode.com/
endpoints
Think of a restaurant menu: Pizza, Burger, Chicken, Juice ā each dish is a menu item. In our app, each endpoint is one thing we can ask the server for: Get Students, Get Teachers, Delete Student, and so on.
builder.query()
This means "I only want to READ." It's like asking your teacher, "Can I see today's attendance list?" ā you're not changing anything, just looking.
query: () => "users"
This tells RTK Query which path to visit. It joins the base URL and this path together:
https://jsonplaceholder.typicode.com/ + users
= https://jsonplaceholder.typicode.com/users
That's the actual address it fetches.
Step 4: Add the API to Your Store
Create store.js:
import { configureStore } from "@reduxjs/toolkit";
import { studentApi } from "./services/studentApi";
export const store = configureStore({
reducer: {
[studentApi.reducerPath]: studentApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(studentApi.middleware),
});
Imagine your teacher asks, "Where should I keep the student register?" You say, "Put it in this cupboard." That's the reducer ā it decides where the data lives.
Now imagine a school messenger: the teacher says, "Take this letter to Class 4," and the messenger delivers it. That messenger is the middleware ā it carries requests to the server and brings the response back.
Step 5: Wrap Your App with Provider
import { Provider } from "react-redux";
import { store } from "./store";
<Provider store={store}>
<App />
</Provider>;
Think of electricity. Without it, your TV, fridge, and computer don't work. Provider supplies the Redux store to your entire app ā without it, nothing connects.
Step 6: Use the Hook
Inside App.jsx:
import { useGetStudentsQuery } from "./services/studentApi";
function App() {
const { data, isLoading, error } = useGetStudentsQuery();
console.log(data);
return <div>Hello</div>;
}
The moment useGetStudentsQuery() runs, RTK Query automatically says "Hello server, can I have the students?" ā no fetch(), no axios(). It's all handled for you.
Step 7: Understanding data
Imagine asking your mom, "Can I have some apples?" and she hands you a bag of ššš. That bag is your data.
For our app, console.log(data) might print:
[
{ "id": 1, "name": "Leanne Graham" },
{ "id": 2, "name": "Ervin Howell" }
]
Step 8: Understanding isLoading
Imagine ordering pizza. Before it arrives, you're waiting ā that's isLoading. While it's on the way, isLoading is true. The moment it arrives, isLoading becomes false.
Step 9: Showing a Loading State
if (isLoading) {
return <h1>Loading...</h1>;
}
Users see "Loading..." until the server responds.
Step 10: Understanding error
If the internet drops or the server fails to respond, error will contain details about what went wrong:
if (error) {
return <h1>Something went wrong.</h1>;
}
Step 11: Rendering the Students
return (
<div>
{data.map((student) => (
<h2 key={student.id}>{student.name}</h2>
))}
</div>
);
If the server returns John and Mary, the screen shows:
John
Mary
What's Actually Happening Behind the Scenes?
š¦ React: I need students.
ā
š RTK Query: I'll ask the server.
ā
š Server: Here are the students.
ā
š¦ RTK Query: I'll save them.
ā
š¦ React: Thank you! I'll show them.
You never wrote fetch(). You never managed a loading flag by hand. You never stored the response yourself. RTK Query handled all of it.
One More Superpower: Caching
Imagine your teacher asks, "What's 2 + 2?" You answer "4." A minute later she asks again ā you already know the answer, so you don't need to think about it again.
RTK Query behaves the same way:
First time:
React ā Server ā Data ā Saved in cache
Second time:
React ā Cache ā
(no new server request)
This makes your app noticeably faster and cuts down on unnecessary network traffic.
Wrapping Up
That's the entire mental model of RTK Query:
-
Define your API with
createApi - Register it in your store (reducer + middleware)
- Provide the store to your app
- Consume it with an auto-generated hook
- Let it cache so repeat requests are instant
Once this clicks, learning mutations (adding, updating, deleting data) is easy ā they follow the exact same pattern, just with builder.mutation() instead of builder.query().
Happy coding! š
Top comments (2)
How does RTK Query handle caching in real-world scenarios, I've had issues with it in the past. Would love to hear your thoughts on this.
I took "real-world" to mean: not the toy example from the article, but what actually happens once you're building a real app with mutations, navigation, multiple users, etc.
I am not sure if thats the explanation you are looking forward to.