How to use RTK Query in a React Application
Data fetching is an essential part of building applications. Most apps require data from numerous sources to display information or conduct actions. Data fetching is critical when developing scalable and maintainable programs. By retrieving data from an external source and caching it in the application, the program can avoid making unnecessary requests and reduce the load on the server. This enhances the overall performance of the application and delivers a better user experience.
In this article, we will learn about RTK Query, a data fetching and caching library from Redux. We will also learn why we probably want to use it in our next application and the numerous alternatives available.
Without further ado, let's get started.
What is RTK Query?
The Redux Toolkit Query, commonly known as RTK Query, is a Redux-integrated data fetching and caching module. It offers a simple and efficient method of managing data from numerous sources. A REST API would be a good example. Because RTK Query employs normalized caching, you can be confident that the data in your store is always up-to-date and consistent.
Normalized caching does this by dividing data into smaller, more manageable chunks and storing them in distinct areas, making it easier to retrieve and handle the data.
Disclaimer โ : For this tutorial, we assume you already have an idea of what Redux is and how it works
Getting Started
Run the following command to setup a Redux + React boilerplate:
# Redux + Plain JS template
npx create-react-app my-app --template redux
# Redux + TypeScript template
npx create-react-app my-app --template redux-typescript
Once complete, you will have a React application with Redux already integrated.
Creating our API slice
We are going to head over to our src\features
folder and create a file called apiSlice
. Feel free to name the file whatever you want if you feel like it.
# apiSlice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const apiSlice = createApi({
reducerPath: 'apiSlice',
baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`
})
}),
})
export const { useGetPostsQuery } = apiSlice
From the code above, we first import createApi
and fetchBaseQuery
from the @reduxjs/toolkit/query/react
library.
We then use the createApi
function to define an API slice with the following configuration options:
The
reducerPath
option specifies the name of the slice's reducer.The
baseQuery
option specifies the base function that will be used to make requests to the server. In this case, it's using thefetchBaseQuery
function from the@reduxjs/toolkit/query
library to set the baseUrl option tohttps://jsonplaceholder.typicode.com/
. This means that all requests made will use this as their base URL.The
endpoints
option is a function that takes abuilder
object as an argument. The builder object provides a set of functions to definequery
,mutation
, andsubscription
endpoints for the API slice.
As a rule of thumb, use the builder.query
function in RTK Query if all you want to do is get data from an endpoint.
Use the builder.mutation
function in RTK Query when you need to perform a server-side mutation that modifies data on the server, such as creating, updating, or deleting resources. This would typically involve making a POST
, PATCH
or DELETE
request to a server endpoint with the user's information in the request body.
We then define a getPosts
endpoint that simply returns the posts
path from the server's API. It does this by querying our baseURL and the additional path we provided. In our case, we provided an additional path of posts
. Hence, the total URL looks something like `https://jsonplaceholder.typicode.com/posts`.
The createApi function generates a set of hooks based on the configuration of the slice. For every builder.query
endpoint we create, a new hook is created for use that prependsuse
and appends Query
to the endpoint's name. In this case, the useGetPostsQuery
hook is generated, because the endpoint's name is getPosts
.
๐ก Tip: Typically, you should only have one API slice per base URL that your application needs to communicate with.
Configuring our store
Now, let's head over to our app/store
file. To configure RTK Query to work with our store
, paste the following code below into your store
.
import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { apiSlice} from '../features/apiSlice'
export const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
return getDefaultMiddleware().concat(apiSlice.middleware);
})
From the above code, we import our apiSlice
. Next, we add apiSlice.reducerPath
property to our reducers
object. The apiSlice.reducerPath
value is then set to the apiSlice.reducer
function, which is the reducer function generated by RTK Query
Then, we set up the middleware specifically for RTK Query. The middleware function first calls getDefaultMiddleware()
to retrieve the default middleware provided by Redux. It then uses the concat
method to add apiSlice.middleware
to the end of this array. The apiSlice.middleware property contains the middleware generated by RTK Query.
Using RTK Query to fetch data
Next, we are going to head over to our src/App
file and paste the following code
import React from 'react'
import { useGetPostsQuery } from './features/apiSlice'
function App() {
const { data, isLoading } = useGetPostsQuery("");
if (isLoading) {
return <p>is loading...</p>
}
return (
<div>
{data?.map((post: any) => (
<p>{post?.title}</p>
))}
</div>
)
}
export default App
From the code above, we import the useGetPostsQuery
hook from our apiSlice
file. We then de-structure data
and isLoading
from useGetPostsQuery
. Data is the actual data returned from the API. While isLoading is a boolean that checks if the endpoint is still trying to get our data.
RTK Query also provides other ways we can track the state. Such asstatus
, error
and utility booleans like isLoading
, isFetching
, isSuccess
, and isError
.
Bonus: Mutating data
Apart from fetching data from our API, let's try to add a new post. We are going back to our src\features\apiSlice
file and add the following code
export const apiSlice = createApi({
...
endpoints: (builder) => ({
...
addPost: builder.mutation({
query: ({ ...rest }) => ({
url: '/posts',
method: 'POST',
body: rest,
}),
})
}),
})
export const { ..., useAddPostMutation } = apiSlice
As you can recall, we said that the builder.mutation
function is used when we want to modify data on the server. Hence, that's why we used it above. We then pass the following configuration options to the builder.mutation
function.
The query
option is a function that takes an object as its argument, which contains any data that needs to be sent to the server. In this case, the object is de-structured using the spread operator ({ ...rest })
to include any additional fields that may be passed to the query.
The query
function then returns an object with the following properties:
The
url
property specifies the URL that the request should be sent to. In this case, it's set to '/posts', which means that the request will be sent to thehttps://jsonplaceholder.typicode.com/posts
endpoint on the server.The
method
property specifies the HTTP method that should be used to send the request. In this case, it's set toPOST
, which means that the request will be a POST request.The
body
property specifies the data that should be sent in the request body. In this case, it's set to the rest object, which contains any additional data that was passed to the query.
Whenever we use the builder.mutation
function to create an endpoint, a hook gets created for us. The naming convention of the hook is as follows: use
gets prepended to our endpoint name. While Mutation
gets appended to the name. In essence, since our endpoint's name is addPost
, the hook generated will be useAddPostMutation
.
Now, we are going to head over to our src\App
file and paste the following code
import React from 'react'
import { useAddPostMutation } from './features/apiSlice'
function App() {
const [addPost] = useAddPostMutation();
const addNewPost = () => {
addPost({
userId: 80,
id: 80,
title: "Test title",
body: "Test body"
}).then((res) => {
console.log(res);
})
}
return (
<div>
<button onClick={addNewPost}>Add post</button>
</div>
)
}
export default App
From the above code, we import the useAddPostMutation
hook from the ./features/apiSlice
file.
We then use the useAddPostMutation
hook to create a addPost
mutation function.
Finally, we add a button that, when clicked, will call the addNewPost
function to initiate the addPost
mutation.