Code Generationでストアを書かないReact開発

2024/02/06に公開

はじめに

かれこれ5年くらいはreduxを書いています。
action書いて、reducer書いて、dispatch叩いて…といったコード量の多い作業です。

「おまえは今まで書いたactionの数をおぼえているのか?」

最近はreduxのように巨大なストアを用意する仕組みから、コンポーネントごとにAPIをフックできる、より独立した設計が好まれています。
わたしもreact-queryやSWRのような、より軽量なライブラリを用いた開発に移行していたので、reduxを冠するライブラリからはご無沙汰でしたが、現場で利用していたことをきっかけにCode Generationを知ることができました。

Code Generate

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

Code GenerationはGraphQLやOpenAPIの定義をもちいてAPIの呼び出しにまつわる諸々のコードを自動生成してくれる機能です。Reactから利用するインターフェイスとしてはカスタムフックまでを提供してくれます。

どんなコードが生成されるのか?

OpenAPIを用意して、そこからコード生成を試してみます。
以下、簡単なGET処理を想定して定義を書いてみました。

openapi: 3.0.0
info:
  title: openapi
  version: '1.0'
servers:
  - url: 'http://localhost:3000/api'
paths:
  '/users/{user_id}':
    parameters:
      - schema:
          type: string
        name: user_id
        in: path
        required: true
    get:
      operationId: get-user-user_id
      tags:
        - User
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
components:
  schemas:
    UserResponse:
      title: UserResponse
      type: object
      properties:
        user:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            email:
              type: string
          required:
            - id
            - name
            - email

自動生成の準備

ファイル構成は以下の通りです。

./<project-root>
├── openapi
│   └── index.yaml
├── openapi-config.ts
├── package-lock.json
├── package.json
└── src
    ├── components
    │   └── App.tsx
    ├── store
        ├── apis.ts
        └── emptyApi.ts

チュートリアルから拝借したemptyApi.tsをstore下に配置します。
こちらにはAPI基底の設定を追加することができます。

// ./src/store/emptyApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const emptySplitApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/' }),
  endpoints: () => ({}),
})

ルート直下にファイル生成のための設定ファイルを配置します。
上記で作ったemptyApi.tsとOpenAPI定義を読み込んで、指定の出力ファイルを生成するための記載をおこないます。

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

const config: ConfigFile = {
  schemaFile: './openapi/index.yaml',
  apiFile: './src/store/emptyApi.ts',
  apiImport: 'emptySplitApi',
  outputFile: './src/ts/store/apis.ts',
  exportName: 'api',
  hooks: true,
}

export default config

ApiProviderをコンポーネント上位に設定する必要があります。
これによって、子となるコンポーネントのどこからでも提供されたカスタムフックを実行することができます。

// src/components/App.tsx
import { ApiProvider } from '@reduxjs/toolkit/query/react';
import React, { FC } from 'react';

import { api } from '@/ts/store/apis';

const App: FC = () => (
  <ApiProvider api={api}>
    <MainComponent />
  </ApiProvider>
);

export default App;

自動生成の実行

以下のコマンドを実行します。

npx @rtk-query/codegen-openapi openapi-config.ts

生成されたコードは以下のとおりです。

import { emptySplitApi as api } from './emptyApi';
const injectedRtkApi = api.injectEndpoints({
  endpoints: (build) => ({
    getUsersByUserId: build.query<
      GetUsersByUserIdApiResponse,
      GetUsersByUserIdApiArg
    >({
      query: (queryArg) => ({ url: `/users/${queryArg.userId}` }),
    }),
  }),
  overrideExisting: false,
});
export { injectedRtkApi as api };
export type GetUsersByUserIdApiResponse = /** status 200 OK */ UserResponse;
export type GetUsersByUserIdApiArg = {
  userId: string;
};
export type UserResponse = {
  user?: {
    id: string;
    uuid: string;
    name: string;
    email: string;
  };
};
export const { useGetUsersByUserIdQuery } = injectedRtkApi;

emptySplitApiがimportされています。こちらには先に生成した createApi が置かれており、APIの基底を生成することができます。

/users/${queryArg.userId} はOpenAPI定義より生成されたAPIのエンドポイントです。
リクエストパラメータの型情報は GetUsersByUserIdApiArg に生成されています。
レスポンスの型情報は GetUsersByUserIdApiResponse に生成されています。
最後に、 useGetUsersByUserIdQuery という名称のカスタムフックとしてexportされています。

ちなみに、Hooksの名称はOpenAPI内の operationId から由来して生成されます。
非常にシンプルですが、必要な型情報とAPI実行が可能なHooksが生成されました!

customHooksの実行

customHooksの呼び出しは以下のような形でおこないます。

const MainComponent: FC = () => {
  const {
    data: { user } = {},
    error,
    isLoading,
  } = useGetUsersByUserIdQuery({ userId: '1' });

  return (
    :
  );
};

export default MainComponent;
  • isLoading が真であるうちはデータがundefinedになります。
  • 取得に成功した場合は data からレスポンスを受け取れます。
  • 失敗した場合、 error にHTTPステータス等の情報が送られてきます。

SWRなどに非常に似たインターフェイスで、昨今のReactで用いやすいcustomHooksになっています。

フロントエンジニアもAPI設計に参加すべき!

OpenAPI定義はバックエンド管轄であることが多かったのですが、これだけ恩恵の大きなジェネレータがあることを知り、フロントエンジニアがOpenAPIを操作するメリットを非常に強く感じることができました。

直近の現場では開発担当をフロント・バックエンドと分担しながらもOpenAPI定義の作成はペアプロで進めています。

総括

今回の記事はほぼチュートリアルの記載になってしまいました。
自身の知識を深めるためのメモなのでご了承くださいw

それ以外には、「ジェネレータを利用することで作業から設計に注力できる」という点こそが大きな見返りだったので、記事を通して伝えられればと思いました。
インターフェイスの意思疎通を実作業を通しておこなうことで、フロント・バックエンド開発がより効率的に、シームレスになることこそジェネレータ利用の醍醐味かもしれません!

Discussion