☀️ Introduction
Finally, I have a chance to continue my Redux blog series. If you haven't read it, I suggested to read the first blog first here. You must understand the Redux concept before reading this blog. To be honest, I was planning to have a blog about Thunk before Writing Redux Toolkit Query, but I was thinking Redux Toolkit Query is more powerful rather than learning Thunk again. However, leave me a comment if you are interested about Thunk. 😀
My goal in this blog is explaining the very basic concept and the easy step to implement the first Redux Toolkit Query. I plan to explain another detail about it in the next blog.
⁉️ What is Redux Toolkit Query/RTK Query?
According to Redux Toolkit documentation,
RTK Query is a powerful data fetching and caching tool. It is designed to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.
This feature is an optional add-on in the Redux Toolkit package, so if you are using Redux Toolkit in your project, it means your project has access to the RTK query.
Maybe, some of you already heard about React Query or SWR. I believe those state management package have the same concept with the RTK Query. However, a winning point about RTK query is all in one with Redux. If you are using Redux, so it's a free optional feature without installing a new package.
⁉️ Why do you need RTK Query?
Let's take a look the simple data fetch in the code below.
It is just simple fetch request. When you are doing a fetch request, does that simple fetch request is enough?
How about these features:
- Fetch loading
- Error handling
- Caching
🔺 Fetch Loading and Error Handling
Okay, I can handle the fetch loading, and error handling. Maybe your code will look like this.
Our states are getting bigger and also the useEffect. This is why RTK Query could solve our problem. You could learn the detail from RTK Query motivation.
🔺 Caching
You can skip this part if you are understand why we need caching
I tried to explain this because I know that myself as a React Junior Dev will not understand why I need this.
Let's think about this. When do we need to fetch the data for the second time? It should be when the data is updated, correct? As long the data in our database is not changed, and we already fetch the data. Technically, there is no point to retrieve the data again.
Make sure, you are agree with the concept above first.
Let's take a look my last code sandbox again. Where I call the fetch data? It's in the useEffect
with an empty array dependency, which means it will fetch the data whenever the component is mounted. Therefore, If the component is unmounted and mounted again, it will fetch the data again.
In order to prevent the fetch again, we should have a caching functionality. Rather than we fetch all the time to the server, we should utilize the cache data. This is another feature that RTK query has, so we don't need to think to create a new caching functionality.
⭐ Implementation
Finally, This is the best part. If you already read my previous blog about Redux Toolkit, you will be familiar with the beginning steps because it's similar.
♦️ Run this command in the terminal
npm install @reduxjs/toolkit react-redux
For the next steps, you can fork from my starting branch to follow my tutorial.
I prepared a json-server with the data, so if you run npm start
, there is an API is running on http://localhost:8000/
. json-server prepared CRUD endpoint for us, and the data will be save in a json file. Before starting the RTK Query implementation, I recommend to tweak the json-server first.
♦️ Create the first API Service
src/app/services/jsonServerApi.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const jsonServerApi = createApi({
reducerPath: 'jsonServerApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8000/' }),
endpoints: (builder) => ({
getAlbums: builder.query({
query: (page = 1) => `albums?_page=${page}&_limit=10`,
}),
}),
});
export const { useGetAlbumsQuery } = jsonServerApi;
I created a query is called getAlbums
with a page
parameter, and it will return 10 records because I limit the API.
Because we are creating a query for fetching data, we need to export a function at the end with adding a prefix and suffix. use
+ endpoints attribute name (getAlbums)
+ Query
= useGetAlbumsQuery
. This is redux toolkit syntax, so we just need to follow the pattern.
♦️ Create Store and Add Service to the store
src/app/store.js
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { jsonServerApi } from './services/jsonServerApi';
export const store = configureStore({
reducer: {
[jsonServerApi.reducerPath]: jsonServerApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(jsonServerApi.middleware),
});
setupListeners(store.dispatch);
If we compare with Redux Toolkit only, this part is getting more complicated to see. However, the main idea is still same, we need to attach the api that we already created to the reducer. In addition, we need to setup the middleware, and call setupListeners
in the last part.
Just in case you are questioning about attributes in jsonServerApi
? It's because we export the jsonServerApi
, and the createApi
is generated those attributes.
♦️ Wrap App component with the Provider
An easy step here.
// ...
import { store } from './app/store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
♦️ Call the useGetAlbumsQuery
in a component (Queries/Read Operation)
I create a new component file is called Albums
src/components/Albums.js
import { useGetAlbumsQuery } from './app/services/jsonServerApi';
export default function Albums() {
const { data: albums } = useGetAlbumsQuery(1);
return (
<ul>
{albums?.map((album) => (
<li key={album.id}>
{album.id} - {album.title}
</li>
))}
</ul>
);
}
Don't forget to call the Albums
component in the App
component
src/App.js
import Albums from './components/Albums';
function App() {
return (
<div>
<Albums />
</div>
);
}
export default App;
Where is the loading and error handle?
Good catch!
There you go!
import { useGetAlbumsQuery } from '../app/services/jsonServerApi';
export default function Albums() {
const {
data: albums = [],
isLoading,
isFetching,
isError,
error,
} = useGetAlbumsQuery(page);
if (isLoading || isFetching) {
return <div>loading...</div>;
}
if (isError) {
console.log({ error });
return <div>{error.status}</div>;
}
return (
<ul>
{albums.map((album) => (
<li key={album.id}>
{album.id} - {album.title}
</li>
))}
</ul>
);
}
Okay, let's do a step back first. Compare the code above with our first approach using the conventional fetch request. It's using less code, less complicated code, and no state at all!
One thing that I believe about using RTK Query. We don't necessary need to use state for data fetching related.
Could you find out that I change something between the two previous code?
const {
data: albums = [],
isLoading,
isFetching,
isError,
error,
} = useGetAlbumsQuery(page);
I make a default value to be empty string to the album.
Why?
I can remove the Optional chaining (?.) operator whenever I call albums. A clever solution, right?
💎 Pagination
This is just a bonus trick.
I added two buttons and a state for handling the page changing. Yeah, at this point, we need a state because it's not related to data fetching.
// ...
export default function Albums() {
const [page, setPage] = useState(1);
// ...
return (
<div>
// ...
<button
disabled={page <= 1}
onClick={() => setPage((prev) => prev - 1)}
>
Prev
</button>
<button
disabled={albums.length < 10}
onClick={() => setPage((prev) => prev + 1)}
>
Next
</button>
</div>
);
}
♦️ Mutation / Create Update Delete Operation
We are entering the fun part here. In the real application, for sure, we need to add new data, update existing data or maybe delete a data. Whatever changes that we made in database, it defines as a mutation in RTK Query.
🟠 Create
Let's create a new component for creating a new album.
This is still the empty html without any logic.
src/components/NewAlbumForm.js
import React from 'react';
export default function NewAlbumForm() {
return (
<form>
<h3>New Album</h3>
<div>
<label htmlFor='title'>Title:</label>{' '}
<input type='text' id='title' />
</div>
<br />
<div>
<input type='submit' value='Add New Album' />
</div>
</form>
);
}
We need to create a new endpoint in the jsonServerApi.js
. However, we are not creating query endpoint anymore, but we create a mutation endpoint.
export const jsonServerApi = createApi({
reducerPath: 'jsonServerApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8000/' }),
endpoints: (builder) => ({
getAlbums: builder.query({
query: (page = 1) => `albums?_page=${page}&_limit=10`,
}),
createAlbum: builder.mutation({
query: (title) => ({
url: `albums`,
method: 'POST',
body: { title },
}),
}),
}),
});
export const { useGetAlbumsQuery, useCreateAlbumMutation } =
jsonServerApi;
One thing that you need to keep in mind. When we call the mutation from the component, we only can send one parameter. For this case, I only send a string because we only need to save a title. We can also make the parameter to be an object and use a destructuring assignment to access the attribute easily.
For example
createAlbum: builder.mutation({
query: ({ title, description, createdBy }) => ({
url: `albums`,
method: 'POST',
body: { title, description, createdBy },
}),
Now, we can add call the mutation in our NewAlbumForm
component
import React from 'react';
import { useCreateAlbumMutation } from '../app/services/jsonServerApi';
export default function NewAlbumForm() {
const [createAlbum, { isLoading }] = useCreateAlbumMutation();
function submitAlbum(event) {
event.preventDefault();
createAlbum(event.target['title'].value);
event.target.reset();
}
return (
<form onSubmit={(e) => submitAlbum(e)}>
<h3>New Album</h3>
<div>
<label htmlFor='title'>Title:</label>{' '}
<input type='text' id='title' />
</div>
<br />
<div>
<input type='submit'
value='Add New Album'
disabled={isLoading}
/>
{isLoading && ' Loading...'}
</div>
</form>
);
}
Let's try the apps now. There is one thing that I want to show you here in this implementation.
Try these steps:
- Go to the last page 11.
- Add a new title.
- Submit.
Do you find something weird?
Could you guess what's happening in here?
The data is not updated even though we change the page.
Caching!
Our data still refers to Caching data, and we don't hit to the backend to invalidate our data.
Let's step back again to the jsonServerApi.js
. We miss something there.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const jsonServerApi = createApi({
reducerPath: 'jsonServerApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8000/' }),
tagTypes: ['Albums'],
endpoints: (builder) => ({
getAlbums: builder.query({
query: (page = 1) => `albums?_page=${page}&_limit=10`,
providesTags: ['Albums'],
}),
createAlbum: builder.mutation({
query: (title) => ({
url: `albums`,
method: 'POST',
body: { title },
}),
invalidatesTags: ['Albums'],
}),
}),
});
export const { useGetAlbumsQuery, useCreateAlbumMutation } = jsonServerApi;
After adding those codes, the results will be different. Give it a shot now.
I'm using a throttling in browser dev tools, so we can see the list whether it is loading or not. If there's a loading, it means we fetch a data to the API. After we are doing a mutation, all getAlbums
query will be updated when we call the query.
🟠 Update and Delete
I believe if you can handle the createAlbum
mutation, you can also handle the updateAlbum
and deleteAlbum
mutation.
I will share the RTK query endpoint to you, but I will leave the implementation to you. However, if you want to see how I have done, you can check my main branch.
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const jsonServerApi = createApi({
reducerPath: 'jsonServerApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:8000/' }),
tagTypes: ['Albums'],
endpoints: (builder) => ({
getAlbums: builder.query({
query: (page = 1) => `albums?_page=${page}&_limit=10`,
providesTags: ['Albums'],
}),
createAlbum: builder.mutation({
query: (title) => ({
url: 'albums',
method: 'POST',
body: { title },
}),
invalidatesTags: ['Albums'],
}),
updateAlbum: builder.mutation({
query: ({ id, title }) => ({
url: `albums/${id}`,
method: 'PUT',
body: { title },
}),
invalidatesTags: ['Albums'],
}),
deleteAlbum: builder.mutation({
query: (id) => ({
url: `albums/${id}`,
method: 'DELETE',
}),
invalidatesTags: ['Albums'],
}),
}),
});
export const {
useGetAlbumsQuery,
useCreateAlbumMutation,
useUpdateAlbumMutation,
useDeleteAlbumMutation,
} = jsonServerApi;
🌙 Conclusion
Finally, we cover all the CRUD operations, and I hope it will be helpful to understand the basics of RTK Query. At least, you can start to set up the RTK Query from the ground. I haven't explained anything in detail about all the hooks and RTK Query documentation. I should separate it from another blog. Therefore, please let me know if you have any questions or suggestions about this blog. Leave a comment! ❇️
Good luck and see you in the next series! 👋
raaynaldo / rtk-query-setup-tutorial
rtk-query-setup-tutorial
RTK Query Setup Tutorial
Getting Start
Install all Packages
npm install
Run the project
npm start
Runs the app in the development mode.
Open http://localhost:8001 to view it in your browser.
json-server will serve in http://localhost:8000
json-server Routes
GET /Albums
GET /Albums/1
POST /Albums
PUT /Albums/1
DELETE /Albums/1
Access the data in db.json
file
Top comments (20)
Thank you so much for this. Will there be content introducing error handling while using mutation with RTK query?
I tried to log the result inside a useEffect like below but the result.isSuccess and result.isError is always being false, even when the data is created and invalidated successfully. What could be the issue?
const [createAlbum, result] = useCreateAlbumMutation()
useEffect(() => {
console.log(result)
}, [result]);
Hi @yanwongpyw , thank you for your comment.
Are you trying to make the isError to be true? I think you can try to update the URL of the mutation to be the wrong URL, I believe the error handling will be working.
Also if you are using RTK Query you don't need to use UseEffect for checking the data. RTK Query has their own dev tool.
github.com/reduxjs/redux-devtools
You can check the status from there.
Tnks its very useful and best then RTK Query docs.
Great job , best description about rtk query.
Thank you Ray, I'm handling the error state, now I'm kinda find some idea to hack :)
Good blog Ray. We will except more on RTK query
Thanks for the content. Great Job Sir!
Following your tutorial, I am trying to use RTK Query to in todo application. However, an error pops up anytime I import the RTK Query's auto-generated hook into a component.
which step is it? I'm not sure if I can debug from this error.
Great job! I will just left here a quick fix to do.
When you mentioned "We can also make the parameter to be an object and use a spread operator to access the attribute easily". I think what you would like to say is "destructuring assignment" instead of "spread operator" (in regards the below snippet)
Again, good job!
Thank you @eduardoklein . Good catch. Appreciate your feedback.
Can someone explain me more about the tags ? invalidate tags, provided tags etc
Hey sorry for late response. the purpose of tags is to validate the data. Because we are getting caching data in RTK Query rather than fetching to API all the time.
Provided Tags initializes the tag name when we need to invalidate the data.
Invalidate Tags will trigger the tag name is provided.
We will use
provided tags
in Query, and invalidateinvalidate tags
in mutation.What about authorization token and refresh token, How can we handle that ?