2022年Reactを使ってる人には必ず知っていてほしい最強のdata fetchingライブラリであるRTK Queryの優位性とメンテナンスの際に役立つTips

## はじめに

筆者はOpenAPIスキーマからRTK Queryのコードを生成する[rtk-query-codegen-openapi](https://github.com/rtk-incubator/rtk-query-codegen/pulls?q=is%3Apr+author%3Akahirokunn+is%3Aclosed)に2020年頃からコントリビュートを続けていました.
rtk-query-codegen-openapiは現在rtk-incubatorリポジトリではなく、[redux-toolkit/packages/rtk-query-codegen-openapi](https://github.com/reduxjs/redux-toolkit/tree/master/packages/rtk-query-codegen-openapi)に統合されています.

今回は、`RTK Query`を幾つかの現場で本番運用して得られた優位性を公開します.

※ 記事の内容に意見がありましたら直接編集リクエストをください

## 他ツールとの機能比較

RTK QueryとReact Queryが作成したマトリックスがある為、リンクだけ貼って省略します

RTK Queryが作成したマトリックス
https://redux-toolkit.js.org/rtk-query/comparison#comparing-feature-sets

React Queryが作成したマトリックス
https://react-query.tanstack.com/comparison

## Best Futures of RTK Query

他のdata fetchingライブラリと比べても、特に強力である機能を選んで紹介します.

### GraphQL Code Generation

GraphQLからコード生成できます

https://redux-toolkit.js.org/rtk-query/usage/code-generation#graphql)

### OpenAPI Code Generation

OpenAPIからこれ以上ない程にコンパクトで、型安全で、明瞭なコードを生成できます.

https://redux-toolkit.js.org/rtk-query/usage/code-generation#openapi)

:::note info
むしろ他でこれができなかったので、コントリビュートし、実用化までの時間を短くしました.
再帰的な木構造を含むOpenAPIからこれ以上の品質のTypeScriptのコードを生成できる物があったら紹介してほしいです.
:::

以下の設定ファイルを用意して、`npx @rtk-query/codegen-openapi openapi-config.ts`を実行すると、この様に余分なコードが一切含まれないRTK Queryのコードが生成されます. その為、OpenAPIを書けば、１行もdata fetchingロジックを書く事がなくなります.

:::note info
endpointsからhooksまで自動生成できるので、RTK Queryでendpointsの書き方は覚えなくても大丈夫です.
:::

```ts:openapi-config.ts
import type { ConfigFile } from "@rtk-query/codegen-openapi";

// https://redux-toolkit.js.org/rtk-query/usage/code-generation#simple-usage
const config: ConfigFile = {
  schemaFile: "https://petstore3.swagger.io/api/v3/openapi.json",
  apiFile: "./store/emptyApi.ts",
  apiImport: "emptySplitApi",
  outputFile: "./store/petApi.ts",
  exportName: "petApi",
  hooks: true,
};

export default config;
```

```ts:petApi.ts
import { emptySplitApi as api } from "./emptyApi";
const injectedRtkApi = api.injectEndpoints({
  endpoints: (build) => ({
    updatePet: build.mutation<UpdatePetApiResponse, UpdatePetApiArg>({
      query: (queryArg) => ({ url: `/pet`, method: "PUT", body: queryArg.pet }),
    }),
    addPet: build.mutation<AddPetApiResponse, AddPetApiArg>({
      query: (queryArg) => ({
        url: `/pet`,
        method: "POST",
        body: queryArg.pet,
      }),
    }),
    findPetsByStatus: build.query<
      FindPetsByStatusApiResponse,
      FindPetsByStatusApiArg
    >({
      query: (queryArg) => ({
        url: `/pet/findByStatus`,
        params: { status: queryArg.status },
      }),
    }),
    findPetsByTags: build.query<
      FindPetsByTagsApiResponse,
      FindPetsByTagsApiArg
    >({
      query: (queryArg) => ({
        url: `/pet/findByTags`,
        params: { tags: queryArg.tags },
      }),
    }),
    getPetById: build.query<GetPetByIdApiResponse, GetPetByIdApiArg>({
      query: (queryArg) => ({ url: `/pet/${queryArg.petId}` }),
    }),
    updatePetWithForm: build.mutation<
      UpdatePetWithFormApiResponse,
      UpdatePetWithFormApiArg
    >({
      query: (queryArg) => ({
        url: `/pet/${queryArg.petId}`,
        method: "POST",
        params: { name: queryArg.name, status: queryArg.status },
      }),
    }),
    deletePet: build.mutation<DeletePetApiResponse, DeletePetApiArg>({
      query: (queryArg) => ({
        url: `/pet/${queryArg.petId}`,
        method: "DELETE",
        headers: { api_key: queryArg.apiKey },
      }),
    }),
    uploadFile: build.mutation<UploadFileApiResponse, UploadFileApiArg>({
      query: (queryArg) => ({
        url: `/pet/${queryArg.petId}/uploadImage`,
        method: "POST",
        body: queryArg.body,
        params: { additionalMetadata: queryArg.additionalMetadata },
      }),
    }),
    getInventory: build.query<GetInventoryApiResponse, GetInventoryApiArg>({
      query: () => ({ url: `/store/inventory` }),
    }),
    placeOrder: build.mutation<PlaceOrderApiResponse, PlaceOrderApiArg>({
      query: (queryArg) => ({
        url: `/store/order`,
        method: "POST",
        body: queryArg.order,
      }),
    }),
    getOrderById: build.query<GetOrderByIdApiResponse, GetOrderByIdApiArg>({
      query: (queryArg) => ({ url: `/store/order/${queryArg.orderId}` }),
    }),
    deleteOrder: build.mutation<DeleteOrderApiResponse, DeleteOrderApiArg>({
      query: (queryArg) => ({
        url: `/store/order/${queryArg.orderId}`,
        method: "DELETE",
      }),
    }),
    createUser: build.mutation<CreateUserApiResponse, CreateUserApiArg>({
      query: (queryArg) => ({
        url: `/user`,
        method: "POST",
        body: queryArg.user,
      }),
    }),
    createUsersWithListInput: build.mutation<
      CreateUsersWithListInputApiResponse,
      CreateUsersWithListInputApiArg
    >({
      query: (queryArg) => ({
        url: `/user/createWithList`,
        method: "POST",
        body: queryArg.body,
      }),
    }),
    loginUser: build.query<LoginUserApiResponse, LoginUserApiArg>({
      query: (queryArg) => ({
        url: `/user/login`,
        params: { username: queryArg.username, password: queryArg.password },
      }),
    }),
    logoutUser: build.query<LogoutUserApiResponse, LogoutUserApiArg>({
      query: () => ({ url: `/user/logout` }),
    }),
    getUserByName: build.query<GetUserByNameApiResponse, GetUserByNameApiArg>({
      query: (queryArg) => ({ url: `/user/${queryArg.username}` }),
    }),
    updateUser: build.mutation<UpdateUserApiResponse, UpdateUserApiArg>({
      query: (queryArg) => ({
        url: `/user/${queryArg.username}`,
        method: "PUT",
        body: queryArg.user,
      }),
    }),
    deleteUser: build.mutation<DeleteUserApiResponse, DeleteUserApiArg>({
      query: (queryArg) => ({
        url: `/user/${queryArg.username}`,
        method: "DELETE",
      }),
    }),
  }),
  overrideExisting: false,
});
export { injectedRtkApi as petApi };
export type UpdatePetApiResponse = /** status 200 Successful operation */ Pet;
export type UpdatePetApiArg = {
  /** Update an existent pet in the store */
  pet: Pet;
};
export type AddPetApiResponse = /** status 200 Successful operation */ Pet;
export type AddPetApiArg = {
  /** Create a new pet in the store */
  pet: Pet;
};
export type FindPetsByStatusApiResponse =
  /** status 200 successful operation */ Pet[];
export type FindPetsByStatusApiArg = {
  /** Status values that need to be considered for filter */
  status?: "available" | "pending" | "sold";
};
export type FindPetsByTagsApiResponse =
  /** status 200 successful operation */ Pet[];
export type FindPetsByTagsApiArg = {
  /** Tags to filter by */
  tags?: string[];
};
export type GetPetByIdApiResponse = /** status 200 successful operation */ Pet;
export type GetPetByIdApiArg = {
  /** ID of pet to return */
  petId: number;
};
export type UpdatePetWithFormApiResponse = unknown;
export type UpdatePetWithFormApiArg = {
  /** ID of pet that needs to be updated */
  petId: number;
  /** Name of pet that needs to be updated */
  name?: string;
  /** Status of pet that needs to be updated */
  status?: string;
};
export type DeletePetApiResponse = unknown;
export type DeletePetApiArg = {
  apiKey?: string;
  /** Pet id to delete */
  petId: number;
};
export type UploadFileApiResponse =
  /** status 200 successful operation */ ApiResponse;
export type UploadFileApiArg = {
  /** ID of pet to update */
  petId: number;
  /** Additional Metadata */
  additionalMetadata?: string;
  body: Blob;
};
export type GetInventoryApiResponse = /** status 200 successful operation */ {
  [key: string]: number;
};
export type GetInventoryApiArg = void;
export type PlaceOrderApiResponse =
  /** status 200 successful operation */ Order;
export type PlaceOrderApiArg = {
  order: Order;
};
export type GetOrderByIdApiResponse =
  /** status 200 successful operation */ Order;
export type GetOrderByIdApiArg = {
  /** ID of order that needs to be fetched */
  orderId: number;
};
export type DeleteOrderApiResponse = unknown;
export type DeleteOrderApiArg = {
  /** ID of the order that needs to be deleted */
  orderId: number;
};
export type CreateUserApiResponse = unknown;
export type CreateUserApiArg = {
  /** Created user object */
  user: User;
};
export type CreateUsersWithListInputApiResponse =
  /** status 200 Successful operation */ User;
export type CreateUsersWithListInputApiArg = {
  body: User[];
};
export type LoginUserApiResponse =
  /** status 200 successful operation */ string;
export type LoginUserApiArg = {
  /** The user name for login */
  username?: string;
  /** The password for login in clear text */
  password?: string;
};
export type LogoutUserApiResponse = unknown;
export type LogoutUserApiArg = void;
export type GetUserByNameApiResponse =
  /** status 200 successful operation */ User;
export type GetUserByNameApiArg = {
  /** The name that needs to be fetched. Use user1 for testing.  */
  username: string;
};
export type UpdateUserApiResponse = unknown;
export type UpdateUserApiArg = {
  /** name that need to be deleted */
  username: string;
  /** Update an existent user in the store */
  user: User;
};
export type DeleteUserApiResponse = unknown;
export type DeleteUserApiArg = {
  /** The name that needs to be deleted */
  username: string;
};
export type Category = {
  id?: number;
  name?: string;
};
export type Tag = {
  id?: number;
  name?: string;
};
export type Pet = {
  id?: number;
  name: string;
  category?: Category;
  photoUrls: string[];
  tags?: Tag[];
  status?: "available" | "pending" | "sold";
};
export type ApiResponse = {
  code?: number;
  type?: string;
  message?: string;
};
export type Order = {
  id?: number;
  petId?: number;
  quantity?: number;
  shipDate?: string;
  status?: "placed" | "approved" | "delivered";
  complete?: boolean;
};
export type User = {
  id?: number;
  username?: string;
  firstName?: string;
  lastName?: string;
  email?: string;
  password?: string;
  phone?: string;
  userStatus?: number;
};
export const {
  useUpdatePetMutation,
  useAddPetMutation,
  useFindPetsByStatusQuery,
  useFindPetsByTagsQuery,
  useGetPetByIdQuery,
  useUpdatePetWithFormMutation,
  useDeletePetMutation,
  useUploadFileMutation,
  useGetInventoryQuery,
  usePlaceOrderMutation,
  useGetOrderByIdQuery,
  useDeleteOrderMutation,
  useCreateUserMutation,
  useCreateUsersWithListInputMutation,
  useLoginUserQuery,
  useLogoutUserQuery,
  useGetUserByNameQuery,
  useUpdateUserMutation,
  useDeleteUserMutation,
} = injectedRtkApi;
```

### 生成されたHooksの使い方

この様に、とても使いやすい感じです.

```tsx:pet.tsx
import { NextPage } from "next";
import { useFindPetsByStatusQuery } from "../store/petApi";

const Pet: NextPage = (props) => {
  const { data, error, isLoading } = useFindPetsByStatusQuery({
    status: "available",
  });

  return (
    <div>
      <h1>Petstore</h1>
      <p>
        <a href="https://redux-toolkit.js.org/rtk-query/usage/code-generation">
          see the tutorial
        </a>
      </p>
      <div>
        {error ? (
          <>Oh no, there was an error</>
        ) : isLoading ? (
          <>Loading...</>
        ) : data ? (
          <>
            <pre>{JSON.stringify(data, null, 2)}</pre>
          </>
        ) : null}
      </div>
    </div>
  );
};

export default Pet;
```

[引用元](https://github.com/kahirokunn/nextjs-redux-tool-kit/blob/dd4bafaad1a7a0efee72406f4c2d64e19b8f4484/pages/pet.tsx#L5)

#### Skipの仕方

1.skipオプションを使う

```tsx
import { useGetPostQuery } from './api'

function MaybePost({ id }: { id?: number }) {
  // This will produce a typescript error:
  // Argument of type 'number | undefined' is not assignable to parameter of type 'number | unique symbol'.
  // Type 'undefined' is not assignable to type 'number | unique symbol'.

  // @ts-expect-error id passed must be a number, but we don't call it when it isn't a number
  const { data } = useGetPostQuery(id, { skip: !id })

  return <div>...</div>
}
```

2.skipTokenを使う


```tsx
import { skipToken } from '@reduxjs/toolkit/query/react'
import { useGetPostQuery } from './api'

function MaybePost({ id }: { id?: number }) {
  // When `id` is nullish, we will still skip the query.
  // TypeScript is also happy that the query will only ever be called with a `number` now
  const { data } = useGetPostQuery(id ?? skipToken)

  return <div>...</div>
}
```

#### refetchOnMountOrArgChange

apiの引数が全く同一の場合、前回のfetchの結果がGCされていない場合は、前回の結果をすぐさま取得し、再fetchしません.
refetchOnMountOrArgChangeオプションを利用すると、前回の結果を使いつつ、新しい結果の取得が可能になります.

```tsx
import { useGetPostsQuery } from './api'

const Component = () => {
  const { data } = useGetPostsQuery(
    { count: 5 },
    // this overrules the api definition setting,
    // forcing the query to always fetch when this component is mounted
    { refetchOnMountOrArgChange: true }
  )

  return <div>...</div>
}
```

### Automated Re-fetching

以下の様にendpointにタグ付けする事により、Endpointの関係性を定義できます.

[引用元](https://redux-toolkit.js.org/rtk-query/usage/automated-refetching#providing-tags)を少し修正しました

```ts:sample.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
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: ['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: ['Post'],
    }),
  }),
})

export const {
  useGetPostsQuery,
  useGetUsersQuery,
  useAddPostMutation,
  useEditPostMutation,
} = api
```

このタグは、定義に追加するだけで、`addPost`や`editPost`を呼び出すのに成功する度に、`getPosts`を呼び出しているコンポーネントがある場合、自動でrefetchしてくれます.

![Shared Private Board@2x (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141711/15d4a323-315e-8c3a-49a2-c8704f1a1c21.png)

この`Automated Re-fetching`機能は、他のdata fetchingライブラリには実装されていない、特に気にいっている強力な機能です.

#### サンプル

以下と同じ特徴を持つ要件を満たす疑似コードが、React QueryとRTK Queryではどうなるのかを紹介します

![Shared Private Board@2x (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141711/5b77d979-2c22-6fb2-1620-1ff2284495f2.png)

#### React Query

[引用元](https://github.com/TanStack/query/blob/f77f6f4f924d7aa1107cbb9bb2dfd09f81beddd8/src/reactjs/tests/useQuery.test.tsx#L1014)

react queryでは、refetchをしたいタイミングで、以下の様に`queryClient.refetchQueries`を利用する必要があります.
なので、例えば`DELETE: /items`をした場合、`queryClient.refetchQueries`を忘れずに毎回呼び出す必要があります.

```jsx
function Page() {
  const result = useQuery(
    key,
    async () => {
      await sleep(10)
      return 'fetched'
    },
    {
      initialData: 'initial',
      staleTime: Infinity,
    }
  )

  return (
    <div>
      <div>isFetching: {result.isFetching}</div>
      <button onClick={() => queryClient.refetchQueries(key)}>
        refetch
      </button>
      data: {result.data}
    </div>
  )
}
```

#### RTK Query

RTK Queryではrefetchのコードを1行も書く必要がありません.
editPostが成功した場合、getPostは自動でrefetchされます.
しかも、hooksもendpoint定義も、全て生成可能です。

```tsx
import { useGetPostQuery, useEditPostMutation } from "./sample.ts"

function Page() {
  const { data, error, isFetching } = useGetPostQuery()
  const [editPost] = useEditPostMutation()

  return (
    <div>
      <div>isFetching: {isFetching}</div>
      <button onClick={() => editPost.unwrap()}>
        edit post
      </button>
      data: {data}
    </div>
  )
}
```

### Automated Re-fetchingの為のタグのコードを生成

私が作成した以下のPRがマージされました.

[rtk-query-codegen-openapi: If tags are specified, they are now mapped to providesTags or invalidatesTags.](https://github.com/reduxjs/redux-toolkit/pull/2071)

つまり、次の`@rtk-query/codegen-openapi`のリリースで、OpenAPIにタグを書いている場合は、オプションを有効化すれば、`providesTags`と`invalidatesTags`を自動で付与できる様になり、RTK Queryを利用している現場のフロントエンドエンジニアはrefetchのコードを自分で書く事はほぼ無いでしょう.

## Customizing queries

この例では `fetchBaseQuery` をラップして、 `401 Unauthorized` エラーが発生したときに追加のリクエストを送信して認証トークンの更新を試み、再認証後に最初のクエリを再試行するようにしています。

[引用元](https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#automatic-re-authorization-by-extending-fetchbasequery)

```ts
import { fetchBaseQuery } from '@reduxjs/toolkit/query'
import type {
  BaseQueryFn,
  FetchArgs,
  FetchBaseQueryError,
} from '@reduxjs/toolkit/query'
import { tokenReceived, loggedOut } from './authSlice'

const baseQuery = fetchBaseQuery({ baseUrl: '/' })
const baseQueryWithReauth: BaseQueryFn<
  string | FetchArgs,
  unknown,
  FetchBaseQueryError
> = async (args, api, extraOptions) => {
  let result = await baseQuery(args, api, extraOptions)
  if (result.error && result.error.status === 401) {
    // try to get a new token
    const refreshResult = await baseQuery('/refreshToken', api, extraOptions)
    if (refreshResult.data) {
      // store the new token
      api.dispatch(tokenReceived(refreshResult.data))
      // retry the initial query
      result = await baseQuery(args, api, extraOptions)
    } else {
      api.dispatch(loggedOut())
    }
  }
  return result
}
```

また、以下の様に指定した回数だけretryする様にする様にする事も可能です.

```ts
import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react'
interface Post {
  id: number
  name: string
}
type PostsResponse = Post[]

// maxRetries: 5 is the default, and can be omitted. Shown for documentation purposes.
const staggeredBaseQuery = retry(fetchBaseQuery({ baseUrl: '/' }), {
  maxRetries: 5,
})
export const api = createApi({
  baseQuery: staggeredBaseQuery,
  endpoints: (build) => ({
    getPosts: build.query<PostsResponse, void>({
      query: () => ({ url: 'posts' }),
    }),
    getPost: build.query<PostsResponse, string>({
      query: (id) => ({ url: `post/${id}` }),
      extraOptions: { maxRetries: 8 }, // You can override the retry behavior on each endpoint
    }),
  }),
})

export const { useGetPostsQuery, useGetPostQuery } = api
```

また、特定のhttp statusやerror時のresponse bodyにキーとなる情報が含まれている場合にretryの挙動を変更する拡張を現在作成中です。

https://github.com/reduxjs/redux-toolkit/issues/2175

## RTK Query + Next.jsの参考リポジトリ

suinさんが作成したリポジトリにパッチを当てたリポジトリを掲載します.
SSR対応済みです.

https://github.com/kahirokunn/nextjs-redux-tool-kit/tree/rtk

## 最後に

RTK Queryの機能はここには書ききれない程多いです.
興味を持った方は詳しくはこちらを参照すると良いでしょう.

https://redux-toolkit.js.org/tutorials/rtk-query