📚 RTK Query Tutorial (CRUD)

📚 RTK Query Tutorial (CRUD)

☀️ 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.

Caching

⭐ 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.

store.js

jsonServerApi.js

♦️ 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>
  );
}

Pagination

♦️ 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>
  );
}

mutation

Let's try the apps now. There is one thing that I want to show you here in this implementation. Try these steps:

  1. Go to the last page 11.
  2. Add a new title.
  3. Submit.

Do you find something weird?

Mutation GIF

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;

Tags

After adding those codes, the results will be different. Give it a shot now.

Mutation GIF 2

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;

update delete

🌙 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! 👋