RTK Query - Redux Toolkit Query tutorial
In this post you will learn what is RTK Query or Redux Toolkit Query and how it can solve your needs.
What is RTK Query?
The first question is what is RTK Query? This is an additional package inside Redux Toolkit library which allows us to simplify fetching data.
If you are familiar with React Query or Apollo Client it is going in the similar direction.
RTK Query simplifies fetching data, caching it, invalidating and reusing.
In this post we won't talk about Redux Toolkit itself. If you don't have enough knowledge about Redux Toolkit and maybe have just knowledge about plain Redux make sure to check this post first.
Real example
To understand RTK Query let's look on my example. Here I already prepared a small project.
Here we load a list of users from API and we can add a new user, send a POST request and it will be saved in database.
So here we make 2 API calls: to get users and to create a user. Let's look on the code.
// src/features/users/Users.js
import { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getUsersAsync, createUserAsync } from "./usersSlice";
const Users = () => {
const dispatch = useDispatch();
const [inputValue, setInputValue] = useState("");
const users = useSelector((state) => state.users.data);
const addUser = () => {
dispatch(createUserAsync(inputValue));
setInputValue("");
};
useEffect(() => {
dispatch(getUsersAsync());
}, [dispatch]);
return (
<div>
<div>Total: {users.length}</div>
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<button onClick={addUser}>Add user</button>
</div>
<div>
{users.map((user, index) => (
<div key={index}>{user.name}</div>
))}
</div>
</div>
);
};
export default Users;
This is our components where we load a list of users and create new user. As you can see we use here Redux Toolkit a lot like dispatch(getUsersAsync())
for example.
// src/features/users/usersSlice.js
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { getUsers, createUser } from "./usersApi";
export const getUsersAsync = createAsyncThunk("users/getUsers", async () => {
const response = await getUsers();
return response.data;
});
export const createUserAsync = createAsyncThunk(
"users/createUser",
async (name) => {
const response = await createUser(name);
return response.data;
}
);
const initialState = {
data: [],
isLoading: false,
error: null,
};
export const usersSlice = createSlice({
name: "users",
initialState,
extraReducers: (builder) => {
builder
.addCase(getUsersAsync.pending, (state) => {
state.isLoading = true;
})
.addCase(getUsersAsync.fulfilled, (state, action) => {
state.isLoading = false;
state.data = action.payload;
})
.addCase(getUsersAsync.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
})
.addCase(createUserAsync.pending, (state) => {
state.isLoading = true;
})
.addCase(createUserAsync.fulfilled, (state, action) => {
state.isLoading = false;
state.data.push(action.payload);
})
.addCase(createUserAsync.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload;
});
},
});
export default usersSlice.reducer;
This is our reducer for users. We have here standard stuff like data
, isLoading
, error
. Also we have here getUsersAsync
and createUserAsync
which creates for us 2 async actions.
This is exactly how we implement working with API without RTK Query
But now we want to refactor this code to use RTK Query. It allows us to write much less code just because we fetch data, make loading indicator and show error message again and again. This is exactly what this library does for us.
- It tracks loading state
- It avoids duplicate requests
- It implements optimistic updates
- It manages cache for requests
But I don't want to show you dry documentation. Let's convert our RTK application to RTK Query.
RTK Query Configuration
Most important point is that we don't need to install anything. We get RTK Query inside RTK package out of the box but if we don't write it's imports it is removed from our build as unused code.
First we can completely remove our usersSlice.js
with all reducer and state logic. It was 53 lines of code that we don't need anymore. So now we have just our component and nothing else.
Now we want to register RTK Query. In order to do that we need a new slice because typically we define a single slice for RTK Query in our application.
// src/features/api/apiSlice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const apiSlice = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:3004" }),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => "users",
})
}),
});
export const { useGetUsersQuery} = apiSlice;
Here we used createApi
function in order to generate a slice. First of all we set reducerPath
which is just a name of the slice. Then we provided baseQuery
. Typically all API requests are coming from a single place this is why it have a lot of sense to set a base query.
After this we have endpoints
. It's the most important property as we define our requests here. First request that we need is getUsers
to fetch the array. As a value we set builder.query
because we make a GET request. Inside it we must define query
property where we set what api we must call. In our case it is /users
.
Additionaly at the bottom of the file we destructure useGetUsersQuery
from our apiSlice
. It allows us to work with fetched data directly in our component.
We don't need to do any changes in our Redux setup but we must provide this api slice to our root reducer.
// src/store.js
import { configureStore } from "@reduxjs/toolkit";
import { apiSlice } from "./features/api/apiSlice";
export const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware),
});
We don't need to register here users slice anymore so we removed it. Instead we provided here a reducer for an API. Additionaly we added here a middleware fromm RTK Query so it is working correctly.
Now we just need to refactor our Users
component.
// src/features/users/Users.js
import { useCreateUserMutation, useGetUsersQuery } from "../api/apiSlice";
const Users = () => {
const [inputValue, setInputValue] = useState("");
const { data } = useGetUsersQuery();
const users = data ?? [];
const addUser = () => {
setInputValue("");
};
console.log("data", users);
return (
<div>
<div>Total: {users.length}</div>
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<button onClick={addUser}>Add user</button>
</div>
<div>
{users.map((user, index) => (
<div key={index}>{user.name}</div>
))}
</div>
</div>
);
};
Here we removed useEffect
and users state as we get our data through useGetUsersQuery
hook. It returns not only data
but also isLoading
, isError
and much more. Because our data
is undefined by default we check that and set an empty array there.
As you can see in browser our code works like previously without any changes.
Here in the devtools we can see 3 actions which we get from RTK Query. Also in the API slice inside we can see fetched data. Now users data is fetched on initialize and is just available in the component without any problems from our side.
Just look how much code we wrote. We don't need reducer anymore at all and any code regarding data fetching, useEffects or syncing state in the component.
Mutations
But we just did a half of our project. But we didn't implement creation. This is why let's implement a new endpoint to do creation of the user.
export const apiSlice = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:3004" }),
endpoints: (builder) => ({
...
createUser: builder.mutation({
query: (name) => ({
url: "users",
method: "POST",
body: { name },
}),
}),
}),
});
export const { useGetUsersQuery, useCreateUserMutation } = apiSlice;
We added createUser
here. As we need to provide method and body we return an object and not just a string. Also we get name
as an argument to create a user. We will provide it when we call a function in our component. Also we exported new useCreateUserMutation
from our apiSlice
.
Now let's update our component.
const Users = () => {
const [inputValue, setInputValue] = useState("");
const [createUser] = useCreateUserMutation();
const addUser = () => {
createUser(inputValue);
setInputValue("");
};
...
}
We got here useCreateUserMutation
which gives us createUser
function as a second argument. By calling it we will trigger createUser
API call.
As you can see in browser we can add a user, we see actions in redux devtools and network request but our list was not updated.
Caching
This is why it is time to talk about caching. This is exactly what happened. We have cached request for the list of users but our mutation (creating of the user) never changed this cached data.
What we need to do instead is to tell RTK Query to invalidate our users list and refetch at after we add new element.
export const apiSlice = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:3004" }),
tagTypes: ["Users"],
endpoints: (builder) => ({
getUsers: builder.query({
query: () => "users",
providesTags: ["Users"],
}),
createUser: builder.mutation({
query: (name) => ({
url: "users",
method: "POST",
body: { name },
}),
invalidatesTags: ["Users"],
}),
}),
});
Here we made 2 changes. First of all we added tagTypes
. In the whole API slice we can define what entities we have and which request does it. After this we added providesTags
in our get request. It means that this request provided Users
tag. With that we can now invalidate this type which we do by adding invalidatesTags
for createUser
.
It means that when we call our createUser mutation our tag Users will be invalidated and data will be automatically refetched.
As you can see RTK Query is a really versatile tool where we are writing much less code.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done