Implementing RTK Query in a React Native App
Published: 2021-12-11 10:30:27

---
title: Implementing RTK Query in a React Native App
published: true
date: 2021-12-10 00:00:00 UTC
tags: 
description: Setting up query and mutation endpoints, with a bit of Jest testing
canonical_url: https://bionicjulia.com/blog/implementing-rtk-query-in-react-native-app
---

I had a bit of downtime in between feature builds last week, so I decided to do a time-boxed investigation into how we might improve API querying throughout our React Native app.

We currently use Axios to help with structuring our API calls. Whilst it does what we need it to, there are definitely ways in which we can improve our querying (e.g. through caching), in addition to DRY-ing up our code (e.g. reducing the need to constantly be setting up local states for things like loading and error statuses).

As a starting point, I had heard lots of good things about React Query, but also the newer Redux Toolkit (RTK) Query. Upon doing a quick read, I confirmed both could do the job I needed, but I ultimately decided on RTK Query for a number of reasons:

- We already use Redux Toolkit to help with state management in our app and RTK Query is included in the same package. This meant I didn't have to introduce yet another package to our code base.
- The caching mechanic seemed more straight-forward and easy to understand, to me.
- We can use the Redux DevTools to monitor the query lifecycle.
- Auto-generated React hooks are a nice touch.

If you want to read more about the differences between RTK Query and React Query, check out the docs [here](https://redux-toolkit.js.org/rtk-query/comparison).

Decision made, what I wanted to do next was to:

- See how difficult it was to get RTK Query set up and running in our code base;
- Create a proof of concept (PoC) PR to present to my team mates, on how we'd define GET, POST and PATCH endpoints, and how these would be hooked up to the UI; and
- See how easy it is to write Jest tests.

Despite RTK Query being released fairly recently (I think around June 2021?), I found the documentation to be substantial and easy to understand, with the set up process being pretty straightforward. You can get the full instructions from the [docs](https://redux-toolkit.js.org/tutorials/rtk-query/), but I've included some code here for completeness.

### Step 1: Set up your API

In my case, I needed to get the user's auth token, to then append to the headers when making an API call. Depending on how auth works for your endpoints, you'll probably need to change this.

```tsx
// @app/api/rtkApi.ts

import appConfig from '@app/appConfig'
import { AsyncStorageService } from '@app/AsyncStorageService'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const api = createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: appConfig.apiBase, // e.g. https://yourapi.com
    prepareHeaders: async (headers) => {
      const user = await AsyncStorageService.getStoredData()
      const hasUser = !!user && !!user!.userToken

      if (hasUser) {
        headers.set('Authorization', `Token ${user.userToken}`)
      }

      headers.set('Content-Type', 'application/json')

      return headers
    },
  }),
  endpoints: () => ({}),
  reducerPath: 'api',
  tagTypes: ['Game'],
})
```

### Step 2: Define GET, POST and PATCH endpoints

You might have realised that I left the endpoints blank above. This is because I wanted to inject my endpoints at runtime, after the initial API slice has been defined. My main reason for this was extensibility, as our app has lots of endpoints to call. I wanted my PoC PR to show how we would structure the code realistically. [This page](https://redux-toolkit.js.org/rtk-query/usage/code-splitting) has more information on this.

```tsx
// @app/api/gameApi.ts

import { api } from '@app/api/rtkApi'
import { TGameRequest } from '@app/game/GameRequest'

export const gameApi = api.injectEndpoints({
  endpoints: (build) => ({
    listGames: build.query<TGameRequest[], void>({
      providesTags: ['Game'],
      query: () => '/games/',
    }),
    addGame: build.mutation<string, { payload: Partial<TGameRequest>; userId: string }>({
      invalidatesTags: ['Game'],
      query: ({ userId, payload }) => ({
        body: {
          user: userId,
          ...payload,
        },
        method: 'POST',
        url: '/games/',
      }),
    }),
    updateGame: build.mutation<string, { payload: Partial<TGameRequest>; userId: string }>({
      invalidatesTags: ['Game'],
      query: ({ userId, payload }) => ({
        body: {
          user: userId,
          ...payload,
        },
        method: 'PATCH',
        url: `/games/${payload.id}/`,
      }),
    }),
  }),
  overrideExisting: false,
})

export const { useListGamesQuery, useAddGameMutation, useUpdateGameMutation } = gameApi
```

Some things to point out:

- `listGames` is a GET endpoint. I defined `providesTags` as `Game`. Note that the `Game` tag has to be defined in `tagTypes` within the `createApi` function.
- For the POST and PATCH endpoints, I defined `invalidatesTags` also as `Game`. What this means is that whenever I call the POST and PATCH endpoints successfully, the `listGames` data cache will be invalidated and the GET endpoint will be called again automatically to refresh the data.
- I don't need to call the `listGames` endpoint with any arguments. This is why you see `void` as the second Typescript argument for `build.query`.
- Because of how the rest of my code base is set up, my payload excludes the `userId`. It's simpler if `userId` is sent through as part of the `payload`. 😬
- The `useListGamesQuery`, `useAddGameMutation` and `useUpdateGameMutation` hooks are all auto-generated. The names of these hooks are based on the names of the endpoints. GET endpoints end with `Query`, whereas POST, PATCH (and other endpoints that mutate data) end with `Mutation`. You'll need to be sure you've imported from the package's `react` folder if you want this feature:
  ```tsx
  import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
  ```

### Step 3: Complete the remaining RTK Query setup

Add the name of your API to the reducer.

```tsx
// @app/state/root.ts

import { api } from '@app/api/rtkApi';

...

export default combineReducers({
  [api.reducerPath]: api.reducer,
  // remaining reducers
});
```

Add the required middleware and setup listeners to your store.

```tsx
// @app/state/store.ts

import { api } from '@app/core/api/rtkApi'
import rootReducer from '@app/core/state/root'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { persistReducer, persistStore } from 'redux-persist'

const persistConfig = {
  key: 'root',
  storage: AsyncStorage,
}

const persistedReducer = persistReducer(persistConfig, rootReducer)

const store = configureStore({
  devTools: __DEV__,
  middleware: getDefaultMiddleware({
    serializableCheck: false,
  }).concat(api.middleware), // NOTE this addition
  reducer: persistedReducer,
})

export const persistor = persistStore(store)
setupListeners(store.dispatch) // NOTE this addition

export default store
```

### Step 4: Use the auto-generated hooks in your UI components

I've simplified my UI screen examples as much as possible, to focus on the core RTK Query features. Here's an example of what my list screen looks like, where I'm calling the `listGames` endpoint.

Where I previously would have used `useState` hooks to monitor local state for error and loading statuses (in order to display error messages or loading spinners), this is now provided by the `useListGamesQuery` hook. i.e. this sort of thing is no longer needed:

```tsx
const [error, setError] = React.useState<string>()
const [loading, setLoading] = React.useState<boolean>(true)
```

The other nice thing is that the hook just runs and updates itself automatically - you don't have to wrap it in some kind of `useEffect` hook to be called on the screen mounting or updating.

```tsx
// ListScreen component

import { useListGamesQuery } from '@app/api/gameApi';
// ...other imports

export function ListScreen(props: TProps) {
  const { data, isError, isLoading } = useListGamesQuery();

  return (
    <>
      {isLoading ? (
        <LoadingSpinner />
        ) : (
        {data.map((item) => {
          // ... render your data
        })
      )

      {isError ? <ErrorText>Oops, something went wrong</ErrorText> : null}
    </>
  )
}
```

Moving on to the POST and PATCH endpoints - let's say they're both called in the same screen (the `AddUpdateScreen`). There's a small difference in how you use mutation hooks, as they don't "run automatically" like the query hooks do. You will instead need to specifically call them at the right point. In the example below, this was on the user clicking a submit button.

```tsx
// AddUpdateScreen component

import { useAddGamesMutation, useUpdateGamesMutation } from '@app/api/gameApi';
// ...other imports

export function AddUpdateScreen(props: TProps) {
  const [addGames, { isLoading: addRequestSubmitting }] = useAddGamesMutation();
  const [updateGames, { isLoading: updateRequestSubmitting }] = useUpdateGamesMutation();

  const onSubmit = async (values: IGameData) => {
    const payload = // sanitise the values received

    if (!addRequestSubmitting && !updateRequestSubmitting) {
      if (existingGame) {
        await updateGames({
          userId: props.userId,
          payload,
        });
      } else {
        await addGames({
          userId: props.userId,
          payload,
        });
      }
    }
  };

  return (
    <Button onPress={onSubmit}>
      Submit
    </Button>
  )
}
```

You'll notice that I renamed the `isLoading` destructured prop from the 2 mutation hooks as I wanted to refer to both, in the screen logic. What the `onSubmit` function is trying to do is to first create the payload, and then, assuming a submission is not already happening, to either call the POST or PATCH endpoint.

RTK Query hooks comes with a host of other return values like `isFetching` and `isError`, so check out the docs for [queries](https://redux-toolkit.js.org/rtk-query/usage/queries#frequently-used-query-hook-return-values) and [mutations](https://redux-toolkit.js.org/rtk-query/usage/mutations#frequently-used-mutation-hook-return-values) to see what's available.

### Step 5: Testing

There's not a lot of information in the official docs at the moment, on how to test RTK Query. If you're interested in actually testing that your API endpoints have been set up correctly, and that your auto-generated hooks are working as expected, check out this [Medium article](https://medium.com/@johnmcdowell0801/testing-rtk-query-with-jest-cdfa5aaf3dc1) which I found super helpful.

The other thing I specifically wanted to test was whether the UI was rendering components correctly, depending on the data that was coming back from the query. This was easily achieved by mocking the response from the hook. I used the [jest-fetch-mock](https://github.com/jefflau/jest-fetch-mock) library to help with this. Once you've got that installed, be sure to enable it in your Jest setup file.

```tsx
// In my Jest setup file

require('jest-fetch-mock').enableMocks()
```

This is an example of what my tests looked like (I'm going to assume you're already testing your Redux store and have something like `redux-mock-store` set up). In this case, for each game returned in my GET response, I wanted to check that a `GameRow` is rendered.

```tsx
// In my test file

import * as hooks from '@app/api/gameApi'
import initialState from '@app/store/initialState'
import { GameRow } from '@app/components/GameRow'
import { getDefaultMiddleware } from '@reduxjs/toolkit'
import createMockStore from 'redux-mock-store'

const middlewares = getDefaultMiddleware()
const mockStore = createMockStore(middlewares)
const store = mockStore(initialState) // define your initial state as needed

const RESPONSE_WITH_TWO_GAMES = [
  // define what your expected response should look like i.e. of type TGameRequest as defined in your API endpoint
]

describe('ListScreen tests', () => {
  it('renders 2 rows when 2 games exist', async () => {
    jest
      .spyOn(hooks, 'useListGamesQuery')
      .mockReturnValue({ data: [RESPONSE_WITH_TWO_GAMES], isError: false, isLoading: false })

    const element = (
      <ReduxProvider store={store}>
        <ListScreen />
      </ReduxProvider>
    )
    const instance = renderer.create(element).root

    await act(async () => {
      expect(instance.findAllByType(GameRow).length).toBe(2)
    })
  })

  it('renders 0 doses when 0 doses exist', async () => {
    jest.spyOn(hooks, 'useListGamesQuery').mockReturnValue({ data: [], isError: false, isLoading: false })

    const element = (
      <ReduxProvider store={store}>
        <ListScreen />
      </ReduxProvider>
    )
    const instance = renderer.create(element).root

    await act(async () => {
      expect(instance.findAllByType(GameRow).length).toBe(0)
    })
  })
})
```

## Conclusion

All in all, I really liked working with RTK Query and found it fairly easy to set up. The Redux DevTools were hugely helpful in helping me understand the lifecycle of a query and how caching works, so I'd definitely recommend you install and activate the dev tools if you haven't already.

As we were not using Axios for particularly complex functions, I decided to do away with Axios completely (though you can [use both in tandem](https://redux-toolkit.js.org/rtk-query/usage/migrating-to-rtk-query) if you so prefer). There are also a number of other RTK Query features that I haven't yet had the chance to try out, like [auto-generating API endpoints from OpenAPI schemas](https://redux-toolkit.js.org/rtk-query/usage/code-generation#openapi) and [global error interception and handling](https://redux-toolkit.js.org/rtk-query/usage/error-handling#handling-errors-at-a-macro-level).

I was watching React Conf 2021 a couple of days ago, where they featured React Suspense heavily and mentioned that they're currently working with tools with React Query to make it simple for devs to integrate Suspense. I'm guessing that RTK Query must also be on that list and am really intrigued to see how RTK Query evolves with this. 😄

Any comments / feedback for me? Catch me on https://bionicjulia.com, [Twitter](https://twitter.com/bionicjulia) or [Instagram](https://instagram.com/bionicjulia).