☀️ 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 spread operator 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! 👋