An overview of ReduxToolkit (RTK) Query

リン (linh)
Goalist Blog
Published in
6 min readMar 9, 2023

I. What is RTK Query for

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.
RTK Query is an optional addon included in the Redux Toolkit package, and its functionality is built on top of the other APIs in Redux Toolkit.

In a normal flow, we usually have a state that holds response data, a state that holds loading status, and probably a state to hold error message. We’ll fetch data and catch data, we set the response data to state and display on the ui.

If we need that data in several places, the block of code above ↑ will be repeated several times. Also, sometime, when we modify the data such as add/edit/delete item, we’ll need to fetch the data again when these promises were done.

RTK Query helps us remove all of that.

II. How to implement

RTK Query ofcourse works well with redux, we can also do some config to make it possible to implement on existing project that using axios or api generated from Openapi.

However, i’m going to implement the most basic case just to show how it works. There’ll be a list of events that is going to take place, a simple form to add event and a button to delete event.

1) Create apiSlice

Start by importing createApi and defining an “API slice” that lists the server’s base URL and which endpoints we want to interact with.

// eventApi.ts

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

export const eventApi = createApi({
baseQuery: fetchBaseQuery({baseUrl: BASE_URL}),
endpoints: builder => ({
getEvents: builder.query<EventDataResponse[], void>({
// declare type of response and type of argument, if no argument, use void
query: () => "/", // api endpoint
})
})
})

export const {
useGetEventsQuery
} = eventApi

In case you’re using typescript, you can put type for the query to indicate response type and arguments type. RTK Query create custom hooks based on the method you provided so you can export and use.

2) Wrap the app with ApiProvider

To use those hooks, we need to wrap the app with ApiProvider.

In case you have redux in your existing project, you dont have to wrap the app with ApiProvider, just use normal redux Provider and configure the store. The “API slice” (here called eventApi) also contains an auto-generated Redux slice reducer and a custom middleware that manages subscription lifetimes. Both of those need to be added to the Redux store:

// _app.tsx

import '@/styles/globals.css'
import type { AppProps } from 'next/app'

import {ApiProvider} from "@reduxjs/toolkit/query/react"
import { eventApi } from '@/apiSlice'

export default function App({ Component, pageProps }: AppProps) {
return (
<ApiProvider api={eventApi}>
<Component {...pageProps} />
</ApiProvider>
)
}

3) Use it

// index.tsx

import Head from 'next/head'
import styles from '@/styles/Home.module.css'
import React from 'react';
import { useDeleteEventMutation, useGetEventsQuery, usePostEventMutation } from '@/eventApi';

export type EventDataResponse = {
id: number;
date: string;
event: string;
no_of_attendants: number;
}

export const BASE_URL = "http://localhost:3500/data";

export default function Home() {

const {data: events, isSuccess, isLoading} = useGetEventsQuery();

return (isLoading ? <span>loading...</span> :
<main className={styles.main}>
<h2>Events</h2>
{
isSuccess &&
<div className={styles.event_list}>
<div className={styles.event_list_item_header}>
<span>Date</span>
<span>Event</span>
<span>Attendants</span>
<span></span>
</div>
{
events.map(ev => (
<div className={styles.event_list_item} key={ev.id}>
<span>{ev.date}</span>
<span>{ev.event}</span>
<span>{ev.no_of_attendants}</span>
<button>x</button>
</div>
))
}
</div>
}
</main>
)
}

As you can see, below are lines of code you can remove after using RTK Query:

//   const [events, setEvents] = React.useState<EventData[]>([]);
// const [isLoading, setIsLoading] = React.useState<boolean>(false);
// const getEvents = () => {
// fetch(BASE_URL)
// .then(res => res.json())
// .then(data => setEvents(data))
// .catch(err => do somthing)
// }

// React.useEffect(() => {
// getEvents();
// },[])

There are also other options from the hooks that you can use, like: isError, isLoading, etc…

4) Adding post and delete method

For POST, PUT, DELETE, we should use builder.mutation instead of builder.query .
And, did you notice:

  • tagTypes below baseQuery
  • providesTags in getEvents
  • invalidatesTags in postEvent and deleteEvent

These are to indicate that whenever postEvent or deleteEvent was called, it will trigger the function that provide the invalidateTag. Which means, when we post an event or delete an event, the events list will be update.

import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
import { BASE_URL, EventDataReq, EventDataResponse } from "./pages";

export const eventApi = createApi({
baseQuery: fetchBaseQuery({baseUrl: BASE_URL}),
tagTypes: ["events"],
endpoints: builder => ({
getEvents: builder.query<EventDataResponse[], void>({
// declare type of response and type of argument, if no argument, use void
query: () => "/", // api endpoint
providesTags: ["events"]
}),
postEvent: builder.mutation<string, EventDataReq>({
query: (req) => ({
url: "/",
method: "POST",
body: req
}),
invalidatesTags: ["events"]
}),
deleteEvent: builder.mutation<string, number>({
query: (id) => ({
url: `/${id}`,
method: "DELETE"
}),
invalidatesTags: ["events"]
})
})
})

export const {
useGetEventsQuery,
usePostEventMutation, // The hook is mutation, not query
useDeleteEventMutation
} = eventApi

III. Other worth-mentioned features

1) Manipulating response data

Endpoints can define a transformResponse handler that can extract or modify the data received from the server before it’s cached.

Ex: sort events data to display newly added event on top by adding transformResponse in getEvent method ike below.
getEvents: builder.query<EventDataResponse[], void>({
query: () => "/",
transformResponse: (res: EventDataResponse[]) => res.sort((a,b) => b.id - a.id),
providesTags: ["events"]
}),

2) Implementing Optimistic Updates

For a small update like adding a reaction, we probably don’t need to re-fetch the entire list of posts. Instead, we could try just updating the already-cached data on the client to match what we expect to have happen on the server. Also, if we update the cache immediately, the user gets instant feedback when they click the button instead of having to wait for the response to come back. This approach of updating client state right away is called an “optimistic update”, and it’s a common pattern in web apps.
RTK Query lets you implement optimistic updates by modifying the client-side cache based on “request lifecycle” handlers. Endpoints can define an onQueryStarted function that will be called when a request starts, and we can run additional logic in that handler.

Because there is no such small update in my example, i’ll implement it on delete method, you can test with your own application.

deleteEvent: builder.mutation<string, number>({
query: (id) => ({
url: `/${id}`,
method: "DELETE"
}),
async onQueryStarted(id, { dispatch, queryFulfilled }){
const optimistUpdate = dispatch(eventApi.util.updateQueryData("getEvents", undefined, draft => {
draft.filter(ev => ev.id !== id);
}))
try {
await queryFulfilled
} catch {
optimistUpdate.undo()
}
},
invalidatesTags: ["events"]
})

What the onQueryStarted is doing is getting a draft data from getEvents and mutate that data directly and return it to display first, while api is still running. By this, the user can see changes immediately, but if the api call was failed, undo() will help to revert the display data to before it was optimistically updated.

3) Others

--

--

リン (linh)
Goalist Blog

A career-changed-non-tech-background point of view.