In this article, we will be learning RTK(Redux Toolkit) Query from scratch by building a small application.
What is RTK Query
In simple words, RTK Query is an extension of Redux Toolkit that simplifies data fetching and caching in your application. It’s designed to make it easier to work with APIs (like fetching data from a server) while keeping the fetched data in sync with your Redux store. Let’s understand how it works
What Problem RTK Query Solves
Problems –
- Tracking loading state by showing the spinner
- Avoiding duplicate requests for the same data
- Managing cache as the user interacts with the UI
Though Redux and createAsyncThunk API provide some or the other things, users still have to write significant amounts of reducer logic to manage the loading state and the cached data.
Bigger Concern
Watch Video
Step 1 – Install Packages
Initialize react app
npm create vite@latest
Install the packages
npm i @reduxjs/toolkit react-redux
Step 2 – Setup json server/backend
Use https://mockapi.io/ to setup a dummy json server
Step 3 – Create API Slice
Importing createApi
and defining an “API slice” that lists the server’s base URL and which endpoints we want to interact with
createAPI –
It’s a function provided by Redux Toolkit Querty that helps to create an API slice.
It takes an object as an argument with 2 key properties –
- baseQuery – Here you define the base URL using fetchBaseQuery for making an HTTP request.
- endpoints – here you define the various endpoints for fetching and receiving data.
Generated Hooks –
Once the slice is defined , redux toolkit queryautomatically exposes a hook for each endpoint, that helps in fetching data in your component.
How to write the hook name ?
Just convert your endpoint name into camel case and add use keyword at the start and Query keyword at end.
use<EndPointName>Query Eg - useGetStudentsQuery
src/components/features/studentApi.tsx
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; export const studentApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://641dd63d945125fff3d75742.mockapi.io' }), endpoints: (builder) => ({ getStudents: builder.query<Student[] , void>({ query: () => '/crud', }), }), }); export const { useGetStudentsQuery } = studentApi ;
The RTK query automatically exposes a hook use<FunctionName>Query which can be used for fetching data.
Step 4 – Configure Store
Create a store
src/app/store.tsx
import { configureStore } from "@reduxjs/toolkit"; import { studentApi } from "../services/api"; // Adding the api middleware enables caching, invalidation, polling, // and other useful features of `rtk-query` export const store = configureStore({ reducer: { [studentApi.reducerPath]: studentApi.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(studentApi.middleware), });
Note you can also use RTK query without store as well, in that case, you don’t need this step.
Step 5 – Provide store to the App
src/index.tsx
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; import { Provider } from "react-redux"; import { store } from "./app/store.ts"; ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> );
Without store
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; import { ApiProvider } from "@reduxjs/toolkit/dist/query/react/ApiProvider"; import { studentApi } from "./services/api.tsx"; ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <ApiProvider api={studentApi}> <App /> </ApiProvider > </React.StrictMode> );
Step 6 –Accessing Data on the UI (GET Request)
The exposed hook automatically handles the loading, error and success state.
src/components/Read.tsx
import { useStudentDataQuery } from "../services/api"; import { useState } from "react"; import "./common/style.css"; const Read = () => { const { data, error, isLoading, isFetching, isSuccess, isError } = useGetStudentsQuery(); return ( <div> <h2>Read Data</h2> {isLoading && <span>Loading..</span>} {isFetching && <span>Fetching Data..</span>} {error && <span>Something went wrong</span>} <div className="main__container"> {isSuccess && data?.map((ele) => ( <div key={ele?.id} className="read__container"> <h2>{ele?.studentName}</h2> </div> ))} </div> </div> ); }; export default Read;
Step 7 – Create a new student (POST request)
Now since “POST or PUT OR PATCH” changes the data, we use the mutation keyword instead of query.
In this case, the query function takes an object with 3 properties-
- URL – the endpoint on which the post action will take place
- method – define the type of HTTP request
- body – The payload or request body which needs to be passed
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; export const studentApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://641dd63d945125fff3d75742.mockapi.io' }), endpoints: (builder) => ({ //get request getStudents: builder.query<Student[] , void>({ query: () => '/crud', }), //post request addStudent: builder.mutation<void, Student>({ query: (student) => ({ url: "/crud", method: "POST", body: student, }), }), }), }); export const { useGetStudentsQuery ,useAddStudentMutation} = studentApi ;
Step 8 – Create a form
unwrap()
When you call a mutation function using Redux Toolkit Query, it returns a Promise that resolves to the full result object. The result object typically includes information about the status of the mutation (e.g., loading, error, or success), as well as the data returned from the server. To access the actual data returned by the server, you use .unwrap().
Example –
try {
const result = await createArticle(newArticleData).unwrap();
console.log('New article created:', result);
} catch (error) {
console.error('Error creating article:', error);
}
src/components/Create.tsx
import React, { useState } from "react"; import { useAddStudentMutation, useStudentDataQuery } from "../services/api"; import { Student } from "../models/student.model"; import { useNavigate } from "react-router-dom"; const Create = () => { const [addStudent] = useAddStudentMutation(); const [students, setStudents] = useState<Student>(Object); // const { refetch } = useStudentDataQuery(); const navigate = useNavigate(); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setStudents({ ...students, [e.target.name]: e.target.value }); }; const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); await addStudent(students); //await addStudent(students).unwrap(); // refetch(); navigate("/"); }; return ( <div> <form onSubmit={handleSubmit}> <div> <input type="text" placeholder="name" name="studentName" onChange={handleChange} /> </div> <div> <input type="email" placeholder="email" name="studentEmail" onChange={handleChange} /> </div> <button type="submit">Submit</button> </form> </div> ); }; export default Create;
Step 9 – Updating and re-fetching
Before understanding the re-fetching concept, let’s first understand the cache behaviour of RTK query
Cache Behaviour
Source: Docs
When data is fetched from the server or backend, RTK Query will store the data in the Redux store as a ‘cache’. When an additional request is performed for the same data, RTK Query will provide the existing cached data rather than sending an additional request to the server.
The cache behaviour and lifetime can be manipulated by keepUnusedDataFor
refetch()
Calling the re-fetch function will forcefully re-fetch the associate query
const { data , refetch } = useStudentDataQuery();
A few other re-fetch methods are – refetchOnMountOrArgChange
, refetchOnFocus
, refetchOnReconnect
Automated Re-fetching in RTK Query
Apart from the above function we also have automated refetch in RTK which works so on the whole API level as well as individual endpoint level.
In Redux Toolkit Query, the concept of "refetching" refers to the ability to manually request or refresh data from an endpoint without relying on the automatic caching and invalidation. This can be useful in scenarios where you need to force a new request to update the data or when you want to provide a user-triggered refresh mechanism.
Additionally, RTK Query allows you to use "tags" to help manage and control the data fetching process.
TagTypes –
Tags are just a label attached to the cached data for controlling caching and invalidation behaviour for re-fetching purposes.
ProvidesTags –
The providesTags option is used to specify the tags that an endpoint provides. These tags represent the data that the endpoint fetches, and they are used for caching and managing data in your Redux store.
The ProvidesTag works on a query endpoint
InvalidatesTags –
The invalidatesTags option is used to specify the tags that should be invalidated when a mutation is performed. Mutations are typically actions that change the data on the server, and you may want to invalidate the cached data in your Redux store to reflect the changes.
The InvalidatesTags works on a mutation endpoint
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; import { Student } from "../models/student.model"; export const studentApi = createApi({ reducerPath: "studentApi", baseQuery: fetchBaseQuery({ baseUrl: "https://641dd63d945125fff3d75742.mockapi.io", }), tagTypes: ["Student"], endpoints: (builder) => ({ getStudents: builder.query<Student[], void>({ query: () => "/crud", providesTags: ["Student"], }), addStudent: builder.mutation<void, Student>({ query: (student) => ({ url: "/crud", method: "POST", body: student, }), invalidatesTags: ["Student"], }), }), }); export const { useStudentDataQuery, useAddStudentMutation, } = studentApi;
In the above code, the getStudents endpoint is associated with the ‘Student’ tag. When data is fetched successfully by this endpoint, it’s stored in the Redux store with the ‘Student’ tag. Other parts of your application can then use this tag to manage the cached data.
In the above code, the addStudent mutation is configured to invalidate the ‘Student’ tag when a new student is created. This means that when the mutation successfully creates a new student data, it triggers the invalidation of any cached data associated with the ‘Student’ tag in the Redux store, so that the next fetch will reflect the updated data.
Step 10 – Transform Response
In RTK Query, the transformResponse option is used to manipulate the response received from an API , before it is used or stored in the Redux store. This can be useful for extracting specific data or headers from the response and performing any necessary transformations.
The “transformResponse” takes 3 parameter –
- response – This contain the raw API response. It includes the response body and header.
transformResponse: (response, meta, arg) => {
// Access response data
const responseData = response.data;
// Access response headers
const responseHeaders = response.headers;
// You can also inspect other properties of the response object as needed.
return response;
}
- meta – It is an object that container details about metadata of request ,response and other related information.
transformResponse: (response, meta) => {
const apiName = meta.apiName;
const path = meta.path;
const requestId = meta.requestId;
// You can access other properties of the meta object as needed.
return response;
}
- args – It contains the argument object that was passed in the query hook when it was dispatched. It if often used to filter the data or manipulated the response .
endpoints: (builder) => ({ studentData: builder.query<Student[], void>({ query: () => "/crud", transformResponse: (res: Student[], meta, args) => { return res.sort((a, b) => a.studentName.localeCompare(b.studentName)); }, providesTags: ["Student"], }),
In the above example, we are sorting the response, before storing it in the redux store.
Depending on your specific use case, you may choose to use one or more of these parameters to perform transformations or other operations on the response before it is stored in the Redux store.
Conclusion
RTK query is basically a redux toolkit with some more added features for data fetching and chachiing.
You cant avoid it because while building bigger application , one major concern is the optimization’s and scalability of an web apps, and feature like this definetly helps alot at that time.
In short , if you are done with redux toolkit , this is your next step to learn, go for it.