React + RTK-Query

React + RTK-Query

RTK query is a tool to retrieve data and cache data which comes with redux toolkit.

What is the need of such a tool?

Applications require to fetch data from servers regularly so that it can be displayed to the users and that data maybe changing as user use the application where the data will be sent to the servers as well. So in such case to provide a seamless experience to the user the data on the UI is required to be cached and that cached data is required to be in sync with the data in the server. Data is cached on the UI so that request does not have to be made repeatedly to fetch same type of data.

So data fetching and caching tools lets developers to not write the data fetching and caching logic manually so that the developers can focus on business logic of the applications. Examples of such tools: Apollo Client, React query, RTK query etc.

In this example, RTK query will be illustrated with react.

RTK Query

RTK query comes with redux toolkit which is an enhancement over redux also lets the developer to write less boilerplate code for managing state. As RTK query comes with redux toolkit so state management can be done with data fetching and caching without using any external tool. Multiple components of the application can also subscribe to the same data.

How RTK Query works

Create a react app first. Name it something. In this scenario app is named as react-rtk-demo. And create necessary folders.

Screenshot 2022-08-06 at 5.19.57 PM.png

This is how the folder structure looks like.

Now adding all the required dependencies for the demo.

npm i @reduxjs/toolkit react-redux

services -> postsApi.js

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

export const postsApi = createApi({
    reducerPath: 'posts',
    baseQuery: fetchBaseQuery({
        baseUrl: "https://jsonplaceholder.typicode.com"
    }),
    tagTypes: ["Posts"],

    endpoints: builder => ({
        getPosts: builder.query({
            query: () => ({
                url: "/posts",
                method: "GET"
            }),
            providesTags: ["Posts"]
        }),

        getSinglePost: builder.query({
            query: id => ({
                url: `/posts/${id}`,
                method: "GET"
            }),
            providesTags: ["Posts"]
        }),

        addPosts: builder.mutation({
            query: payload => ({
                url: '/posts',
                method: "POST",
                body: payload
            }),
            invalidatesTags: ["Posts"]
        }),

        deletePost: builder.mutation({
            query: id => ({
                url: `/posts/${id}`,
                method: "DELETE"
            })
        })
    })
})

export const {
    useGetPostsQuery,
    useGetSinglePostQuery,
    useAddPostsMutation,
    useDeletePostMutation,
} = postsApi

createApi: Allows the developer to define various endpoints which will define how the data will be fetched or transformed from the endpoints. This creates api slice which has to be unique per base url. createApi takes certain parameters defined below.

fetchBaseUrl: It is wrapper which wraps the fetch API. The base url is given to the fetchBaseUrl.

reducerPath: In RTK query, a reducer with name mentioned will be generated.

Tags: Tags are cache tag which provides the feature of automatic re-fetching of the data from the endpoints. If there is any mutation in the data, the cached data will be considered invalidated and in that case data has to be fetched again.

endpoints: The developer can define different endpoints related to the baseUrl here.

  • getPosts: This method will fetch all the posts from the server will not modify any data so query method will be used. The query method will take certain parameters like url, method. The providesTag in this case 'Post' will be used.

  • addPosts: This method will post data to the server and will also take the body of the request as parameter as this endpoint will modify the data so invalidatesTag will be used.

  • deletePost: This method will delete post from the server and will take an id and will delete the post with that id and as it will modify the data so invalidatesTag will be used.

Now once the endpoints are defined then this createApi slice provides different hooks which can be used in react components. In this case the hooks exported will be useGetPostsQuery, useGetSinglePostQuery, useAddPostsMutation, useDeletePostMutation. The naming of these hooks is automatically done based on the names of the endpoint methods. Like for getPosts method, the hook which will be generated will be useGetPostsQuery. Add use before the name of the method in endpoint and add query or mutation based on the type of method after the name of the endpoint method.

Configure the store in the application:

app -> store.js

import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query";
import { postsApi } from "../services/postsApi";

import { postsApi } from "../services/postsApi";

export const store = configureStore({
    reducer: {
        [postsApi.reducerPath]: postsApi.reducer,
    },
    middleware: getDefaultMiddleware => 
        getDefaultMiddleware().concat(postsApi.middleware)
})

setupListeners(store.dispatch)

Export the configureStore from redux toolkit by passing certain parameters to it.

  • Add the reducer which was generated by the postsApi slice.

  • Add the middleware generated by the slice to the list of default middlewares provided redux.

Wrapping the application by store object

This is done so that every component in the application has access to the store.

index.js

<Provider store={store}>
    <App />
</Provider>

Integrating generated hooks in react component

App.js

import React, { useState } from 'react'
import { useAddPostsMutation, useGetPostsQuery, useGetSinglePostQuery, useDeletePostMutation } from './services/postsApi'

const App = () => {

  const [inputs, setInputs] = useState({
    title: '',
    body: ''
  })

  const { data: posts, isLoading: getLoading, isError } = useGetPostsQuery()
  const { data: singlePost } = useGetSinglePostQuery(1)
  const [ addPosts, { isLoading: postLoading } ] = useAddPostsMutation()
  const [ deletePost, { isLoading: deleteLoading }  ] = useDeletePostMutation()

  if (getLoading) return 'LOADING..........'

  const handleChange = e => {
    const { name, value } = e.target

    setInputs(prevState => {
      return {
        ...prevState,
        [name]: value
      }
    })
  }

  const handleSubmit = e => {
    e.preventDefault()

    const { title, body } = inputs
    const dataToBeAdded = {
      userId: 1,
      id: 101,
      title,
      body
    }
    addPosts(dataToBeAdded)
  }

  const handleDelete = id => {
    deletePost(id)
  }

  const { title, body } = inputs

  return (
    <>
      <form onSubmit={handleSubmit}>
        Title: <input type="text" name="title" value={title} onChange={handleChange} />
        Body: <input type="text" name="body" value={body} onChange={handleChange} />
        <button>Add Post</button>
      </form>

      <h1>Single Post</h1>
      <h3>{singlePost?.title}</h3>
      <p>{singlePost?.body}</p>
      <hr />

      <h1>All posts</h1>
      {
        posts?.map(post => {
          return (
            <div key={post?.id}>
              <h3>{post?.title}</h3>
              <p>{post?.body}</p>
              <button onClick={() => handleDelete(post.id)}>Delete</button>
            </div>
          )
        })
      }

    </>
  )
}

export default App

The hooks which generated can be called as function as the result of calling one the hooks is:

Screenshot 2022-08-07 at 3.10.07 PM.png

So this is all the data which is generated and can be used by destructuring from the object. This is the result of useGetPostsQuery hook. isError, isLoading etc. can be very useful while loading data from the server or while transforming any data. These are generated automatically and developer does not have to write any other logic for it.

For the hook useSinglePostQuery the id has to be passed as the argument which will be the id of the post that has to be fetched.

Now the for the mutation hooks, unlike query hooks, mutation hooks will return a tuple which will have a trigger function and an object. Now when the function will be trigger the data which has to be transformed has to be passed to the function that will be considered as the body of the request.

Conclusion

RTK query can be great way to achieve data fetching and caching in the application and if the application uses redux as the state management tool then RTK query comes with redux which allows the developer to not use any other tool for data caching.