This is actually a very good small project to understand how RTK Query works in a real React app. Instead of just reading theory, we now have a full flow:
React Components
β
RTK Query Hooks
β
Redux Store Cache
β
API (json-server)
Weβll walk through every file step-by-step and explain the concepts deeply.
Our db.json
{
"notes": [
{
"id": "1",
"title": "1st Note",
"content": "This is the content of the 1st note"
},
{
"id": "2",
"title": "2nd Note",
"content": "This is the content of the 2nd note"
},
{
"id": "3",
"title": "3rd Note",
"content": "This is the content of the 3rd note"
}
]
}This is a fake REST API database.
When we run:
json-server --watch db.json --port 3000
It automatically creates REST endpoints.
GET /notes
GET /notes/:id
POST /notes
PATCH /notes/:id
DELETE /notes/:id
So our React app can interact with it like a real backend.
This file defines how React talks to the server.
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";This is the main function that creates an API service.
It automatically generates:
- Redux reducers
- Redux middleware
- React hooks
- Caching system
export const notesApi = createApi({
reducerPath: "notesApi",This defines where RTK Query state lives in Redux store.
Example store:
store = {
notesApi: {
queries: {},
mutations: {}
}
}baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:3000/" })fetchBaseQuery is a small wrapper around fetch API.
So internally it does something like:
fetch(baseUrl + endpoint)Example:
baseUrl = http://localhost:3000/
endpoint = notes
Final request:
http://localhost:3000/notes
tagTypes: ["Note"]Tags are used for automatic cache invalidation.
Think of it like:
Query Cache
β
Tagged with "Note"
When we update/delete/add β RTK Query knows which cached data should be refreshed.
Endpoints define API operations.
Query β GET data
Mutation β change data
getNotes: builder.query({
query: () => "notes",
providesTags: ["Note"],
})Used for fetching data (GET requests).
Defines the endpoint path.
GET http://localhost:3000/notes
This marks cached data with tag:
"Note"
Meaning:
notes cache β tag "Note"
Later if a mutation invalidates "Note" β query refetches automatically.
addNote: builder.mutation({
query: (newNote) => ({
url: "notes",
method: "POST",
body: newNote,
}),
invalidatesTags: ["Note"],
})Mutation means changing server data.
Returns request configuration.
POST /notes
Body:
{
title,
content
}
After adding a note:
cache tag "Note" becomes invalid
RTK Query automatically:
refetch getNotes()
So UI updates automatically.
updateName: builder.mutation({
query: ({ id, ...updatedNote }) => ({
url: `notes/${id}`,
method: "PATCH",
body: updatedNote,
}),
invalidatesTags: ["Note"],
})PATCH /notes/1
Body:
{
title,
content
}
This updates the note.
Then:
invalidate "Note" tag
β getNotes refetch
deleteNote: builder.mutation({
query: (id) => ({
url: `notes/${id}`,
method: "DELETE",
}),
invalidatesTags: ["Note"],
})Request:
DELETE /notes/1
After deletion:
invalidate "Note"
β refetch notes
export const {
useGetNotesQuery,
useAddNoteMutation,
useUpdateNameMutation,
useDeleteNoteMutation,
} = notesApi;RTK Query automatically creates hooks.
useGetNotesQuery()
useAddNoteMutation()
useUpdateNameMutation()
useDeleteNoteMutation()
These hooks connect React β Redux β API.
const { data: notes, isLoading } = useGetNotesQuery();This hook automatically:
1οΈβ£ sends API request 2οΈβ£ stores result in Redux cache 3οΈβ£ re-renders component
data: notes
Renaming:
data β notes
if (isLoading) {
return <h2>Loading... β³</h2>;
}RTK Query manages loading automatically.
notes.map((note) => (Example:
[
{id:1,title:"1st"},
{id:2,title:"2nd"}
]
Each note displayed in UI.
const [deleteNote] = useDeleteNoteMutation();Trigger function.
When clicked:
deleteNote(note.id)Flow:
DELETE request
β
invalidate tag
β
getNotes refetch
β
UI updates
Handles creating new notes.
const [title, setTitle] = useState("");
const [content, setContent] = useState("");Controlled form inputs.
const [addNote] = useAddNoteMutation();Trigger function.
await addNote({ title, content });Request:
POST /notes
Body:
{
title,
content
}
After success:
invalidatesTags
β getNotes refetch
So list updates automatically.
Handles updating notes.
const [title, setTitle] = useState(note.title);
const [content, setContent] = useState(note.content);Prefilled form using props.
Correct version:
const [updateNote, { isLoading, isError, error }] = useUpdateNameMutation();This returns:
[
triggerFunction,
{
isLoading,
isError,
error
}
]
await updateNote({
id: note.id,
title,
content
});Request:
PATCH /notes/1
disabled={isLoading}Prevents multiple submissions.
{isError && (
<div>Error: {error.data.message || "Updation-Failed β οΈ"}</div>
)}Shows mutation errors.
This is the root UI.
<NotesList />
<NoteForm />
<EditNote />Components:
NotesList β show notes
NoteForm β add notes
EditNote β update notes
When our app loads:
React mounts
β
useGetNotesQuery()
β
RTK Query sends GET /notes
β
Response cached in Redux
β
NotesList renders
NoteForm
β
addNote()
β
POST /notes
β
invalidatesTags("Note")
β
getNotes refetch
β
NotesList updates
deleteNote(id)
β
DELETE /notes
β
invalidate "Note"
β
getNotes refetch
We actually implemented most RTK Query core concepts:
createApi()
builder.query()
Used for fetching.
builder.mutation()
Used for modifying data.
useQuery
useMutation
providesTags
invalidatesTags
Automatic cache refresh.
RTK Query manages:
loading
error
data
caching
refetching
Without writing reducers.