What is an RTK Query, and why do we need it?

Long website loading times can be frustrating and significantly impact the user experience. The most common approach to fixing those problems is implementing caching mechanisms. It will persist data between fetches so it is available immediately on the next fetch. This simplified explanation of caching mechanisms hides the complexity that it brings; for example, sometimes server state changes and cached data isn’t relevant anymore, so we have to invalidate that cached data in order to refetch the newest updates. Also, after we get fetched data, we would probably have to write logic to store and update that data in state management.

Users should be able to see what is happening with the application after performing an action, and we have to use some state that will be set to loading/error/success. Also, we often encounter problems that are solved by polling. Implementing polling logic isn’t so hard at first, but should your application constantly send requests, what if it is not in focus? Then, sending requests would waste resources. RTK Query is a powerful tool used to solve mentioned problems, and much more for us in React. RTK in RTK Query stands for Redux Toolkit, and as you can imagine, it heavily depends on Redux Toolkit. Do not worry if you aren’t familiar with Redux Toolkit, we have covered it in another blog.

 

Should you use RTK Query?

As any other developer, you probably figured out by now that every problem has more than one solution. Each of the solutions can help you solve the problem, but their effectiveness won’t be the same. The same goes for data fetching and caching problems. In order to determine how efficiently RTK Query will solve your problem, we just have to answer these questions:

Application size and complexity

Does your application have complex data relations where different parts of the applications depend on each other? Are you fetching large amounts of data often? Is the API you are using complex? If you answered any of these questions with yes, then RTK Query might be a great solution for you. In case your application is small and doesn’t need state management and caching, you might be better off with Jotai and Axios or Zustand so you can avoid adding unnecessary complexity to your application.

Are you using or planning to use Redux Toolkit as state management?

As we mentioned earlier, RTK Query requires a Redux Toolkit to work properly, and if you are using a Redux Toolkit, RTK Query is the best solution for you since it integrates with the Redux Toolkit seamlessly. There are many other solutions to state management problems in React other than RTK Query, like Recoil, MobX, Zustand, Jotai, Rematch, and many more. There is a possibility that you might pick some of them. For example, suppose you are building a small project and don’t want to add too much complexity. In that case, you might want to go with Jotai, or if you are working on a medium project and want a good balance between features and complexity, Rematch might be a better choice for you. If you use some other state management than Redux Toolkit, you might want to look into solutions like React Query, SWR, Axios, or self-built  solutions that might fit your needs better than RTK Query.

 

Functionalities Overview

RTK Query will have a solution for almost every problem you encounter. We will go through the most common and most interesting ones in this blog. I would recommend exploring other functionalities like prefetching, conditional fetching, streaming updates, polling, code splitting, and various customization options.

Data fetching

In order to fetch some data, considering we already have the Redux Toolkit setup, all we have to do is follow these simple steps.

Create an API Slice:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'

// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
  reducerPath: 'pokemonApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
  endpoints: (builder) => ({
    getPokemonByName: builder.query<Pokemon, string>({
      query: (name) => `pokemon/${name}`,
    }),
  }),
})

// Export hooks for usage in functional components, which are
// auto-generated hook based on the defined endpoints
export const { useGetPokemonByNameQuery } = pokemonApi

Configure the store:

import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query'
import { pokemonApi } from './services/pokemon'

export const store = configureStore({
  reducer: {
    // Add the generated reducer as a specific top-level slice
    [pokemonApi.reducerPath]: pokemonApi.reducer,
  },
  // Adding the api middleware enables caching, invalidation, polling,
  // and other useful features of `rtk-query`.
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(pokemonApi.middleware),
})

// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch)

Finally, use data where you need it:

import * as React from 'react'
import { useGetPokemonByNameQuery } from './services/pokemon'

export default function App() {
  // Using a query hook automatically fetches data and returns query values
  const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')
  // Individual hooks are also accessible under the generated endpoints:
  // const { data, error, isLoading } = pokemonApi.endpoints.getPokemonByName.useQuery('bulbasaur')

  // render UI based on data and loading state
}


Mutations

Often, in web applications, there is a need for changing server state. That use case is somewhat different from just getting data from the server. Thankfully, RTK Query offers a solution called mutations that help us with performing server state changes. Unlike the mentioned queries, mutations won’t trigger automatically; we are open to implementing logic that will trigger them.  Sometimes, we want to have optimistic updates in our application. For example, if a user makes an edit, we want to show the change in our application immediately, even before the change is live on the server. This is done through mutation queries. In this example, we can see how to define mutation and implement optimistic updates:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post } from './types'

const api = createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: '/',
  }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    // The mutation accepts a `Partial<Post>` arg, and returns a `Post`
    updatePost: build.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
      query: ({ id, ...patch }) => ({
        url: `post/${id}`,
        method: 'PATCH',
        body: patch,
      }),
      transformResponse: (response: { data: Post }, meta, arg) => response.data,
      transformErrorResponse: (
        response: { status: string | number },
        meta,
        arg
      ) => response.status,
      invalidatesTags: ['Post'],
      // onQueryStarted is useful for optimistic updates
      // The 2nd parameter is the destructured `MutationLifecycleApi`
       async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
        const patchResult = dispatch(
          api.util.updateQueryData('getPost', id, (draft) => {
            Object.assign(draft, patch)
          })
        )
        try {
          await queryFulfilled
        } catch {
          patchResult.undo()

          /**
           * Alternatively, on failure you can invalidate the corresponding cache tags
           * to trigger a re-fetch:
           * dispatch(api.util.invalidateTags(['Post']))
           */
        }
      },
      // The 2nd parameter is the destructured `MutationCacheLifecycleApi`
      async onCacheEntryAdded(
        arg,
        {
          dispatch,
          getState,
          extra,
          requestId,
          cacheEntryRemoved,
          cacheDataLoaded,
          getCacheEntry,
        }
      ) {},
    }),
  }),
})



As we can see, in the onQueryStarted callback, we are changing the cached data value in order to have optimistic updates, and in case the request fails, we revert the cache changes.

Automated Re-fetching

RTK Query offers a simple yet powerful solution to invalidating cache and triggering automated re-fetching. First, when creating an API, we have to define which tags we are going to use. That is done through tagTypes property. Then, when specifying a query, we provide tags that we want to attach to specific data, through the provideTags property. Finally, after performing mutation on data, we can use tags to invalidate cached data correlated with that tag, through invalidatesTags property. Here is an example:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post, User } from './types'

const api = createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: '/',
  }),
  tagTypes: ['Post', 'User'],
  endpoints: (build) => ({
    getPosts: build.query<Post[], void>({
      query: () => '/posts',
      providesTags: (result, error, arg) =>
        result
          ? [...result.map(({ id }) => ({ type: 'Post' as const, id })), 'Post']
          : ['Post'],
    }),
    getUsers: build.query<User[], void>({
      query: () => '/users',
      providesTags: ['User'],
    }),
    addPost: build.mutation<Post, Omit<Post, 'id'>>({
      query: (body) => ({
        url: 'post',
        method: 'POST',
        body,
      }),
      invalidatesTags: ['Post'],
    }),
    editPost: build.mutation<Post, Partial<Post> & Pick<Post, 'id'>>({
      query: (body) => ({
        url: `post/${body.id}`,
        method: 'POST',
        body,
      }),
      invalidatesTags: (result, error, arg) => [{ type: 'Post', id: arg.id }],
    }),
  }),
})


Instead of using a specific ID, we can also create another tag, for example, ‘LIST’; which will be an abstraction of a specific data collection, and we can invalidate that whole collection with an abstract tag. In the table below, we can better see ​​which invalidated tags will affect and invalidate which provided tags:

In the table we can better see ​​which invalidated tags will affect and invalidate which provided tags

 


“RTK Query Insights” Tech Bite was brought to you by Nail Bobić, Junior Software Engineer at Atlantbh.

Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.

Leave a comment

Your email address will not be published. Required fields are marked *