デザイン&エンジニアリング部の髙谷です。
先日、Fintanで公開されているReact Nativeのサンプルアプリケーション(以下サンプルアプリケーション)を参考にReact QueryとOpenAPI定義ファイルからコードを自動生成するツールOrvalを使ってWebアプリケーションの通信基盤を作成する機会がありました。React Queryを利用した通信状態の管理とOrvalを利用したコード自動生成の組み合わせは効果的で、開発コストの削減に役立ちました。
この記事ではWebアプリケーションの通信基盤を作成するにあたってサンプルアプリケーションをWebアプリケーション開発に活用する際に参照すべき箇所※1を紹介します。また、React Queryと比べて取り上げている記事がまだ少ないOrvalについて、利用する際のポイントも合わせて紹介します。

React Queryとは

React Query※2 は、Reactで非同期データをフェッチ、キャッシング、更新するためのフックを提供するライブラリです。
React Queryを用いることで以下のようなメリットを得ることができます。
  • 通信結果のキャッシュをアプリケーション単位で管理することで無駄な通信や画面間のデータ受け渡しによる複雑化を避けることができる
  • 非同期通信の状態管理を楽にできる
    他にも様々なメリットがあります。詳細な情報はReact Queryを用いた開発事例の紹介 や React Queryの公式ドキュメントを参照してください。

    Orvalとは

    OrvalはOpenAPI定義ファイルからTypeScriptの型定義、React Queryを利用したHTTP通信処理のコードを自動生成が可能なツールです。

    Orval利用時のポイント

    設定

    Orvalの設定はルートディレクトリに配置したorval.config.tsファイルによって行います。
    設定の中でポイントとなるのはmodecleanです。
    【Orvalの設定例】
    backend: {
      output: {
        mode: 'tags-split',
        clean: true,

    引用元

    mode

    modeは自動生成するコードをどの粒度でファイル分割するかを指定する設定です。デフォルトではsingleに設定されており、自動生成するコードは全て1ファイルに出力されます。
    後述する自動生成されたコードの利用方法でも触れますが、自動生成されたカスタムフックを直接利用せずに途中にService層を挟むことで変更容易性を上げることができます。  
    その際、全てのカスタムフックが1ファイルに出力されているとコードの見通しが悪く実装コストを増加させます。
    お勧めのmodeはtags-splitです。OpenAPI定義のtags毎にディレクトリとファイルが分割され、コードの見通しが良くなります。

    clean

    cleanは自動生成コードの出力先ディレクトリに既に存在するコードを再自動生成前に全て削除するか否かを指定する設定です。
    デフォルトでfalseに設定されており、既存コードは削除されません。  
    自動生成するコードに変更を加えることを許容するとコードがOpenAPI定義と一致していることを保証できなくなり開発者が考慮すべきことが不要に増えてしまいます。この設定はtrueにすることをお勧めします。
    その他の設定はサンプルアプリケーションOrvalの公式ドキュメントを参照してください。

    自動生成されたコードの利用方法

    Orvalによってどのようなコードが生成されるか、生成されたコードをどのように利用するかを紹介します。
    まず生成元となるOpenAPIは以下の通りです。
    以下の例ではtodoIdをもとに情報を取得・更新するAPIを定義しています。
    【生成元となるOpenAPI定義例】
    '/todos/{todoId}':
      parameters:
        - name: todoId
          in: path
          description: Todo ID
          required: true
          schema:
            type: number
      get:
        summary: Get todo
        description: Get todo
        tags: []
        operationId: get-todo
        responses:
          '200':
            description: OK
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/Todo'
    ~中略~
      put:
        summary: Update todo
        description: Update todo
        tags: []
        operationId: put-todo
        requestBody:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TodoRegistration'
        responses:
          '200':
            description: OK
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/Todo'
    上記のOpenAPI定義をもとにOrvalによるコード自動生成すると、以下のようにバックエンドアプリケーションとの通信処理(getTodo, putTodo)、useQueryをベースとしたカスタムフック(useGetTodo, usePutTodo)、クエリのキャッシュを管理するためのQuery Keysを取得する処理(getGetTodoQueryKey)の3種類が作成されます。
    【生成されるコード例】
    /**
     * Get todo
     * @summary Get todo
     */
    export const getTodo = (todoId: number) => {
      return sandboxCustomInstance<Todo>({url: `/todos/${todoId}`, method: 'get'});
    };
    
    export const getGetTodoQueryKey = (todoId: number) => [`/todos/${todoId}`];
    
    export const useGetTodo = <TData = AsyncReturnType<typeof getTodo>, TError = ErrorType<NotFoundResponse>>(
      todoId: number,
      options?: {query?: UseQueryOptions<AsyncReturnType<typeof getTodo>, TError, TData>},
    ): UseQueryResult<TData, TError> & {queryKey: QueryKey} => {
      const {query: queryOptions} = options || {};
    
      const queryKey = queryOptions?.queryKey ?? getGetTodoQueryKey(todoId);
    
      const queryFn: QueryFunction<AsyncReturnType<typeof getTodo>> = () => getTodo(todoId);
    
      const query = useQuery<AsyncReturnType<typeof getTodo>, TError, TData>(queryKey, queryFn, {
        enabled: !!todoId,
        ...queryOptions,
      });
    
      return {
        queryKey,
        ...query,
      };
    };
    
    /**
     * Update todo
     * @summary Update todo
     */
    export const putTodo = (todoId: number, todoRegistration: TodoRegistration) => {
      return sandboxCustomInstance<Todo>({url: `/todos/${todoId}`, method: 'put', data: todoRegistration});
    };
    
    export const usePutTodo = <TError = ErrorType<BadRequestResponse | NotFoundResponse>, TContext = unknown>(options?: {
      mutation?: UseMutationOptions<
        AsyncReturnType<typeof putTodo>,
        TError,
        {todoId: number; data: TodoRegistration},
        TContext
      >;
    }) => {
      const {mutation: mutationOptions} = options || {};
    
      const mutationFn: MutationFunction<
        AsyncReturnType<typeof putTodo>,
        {todoId: number; data: TodoRegistration}
      > = props => {
        const {todoId, data} = props || {};
    
    return putTodo(todoId, data);
      };
    
    return useMutation<AsyncReturnType<typeof putTodo>, TError, {todoId: number; data: TodoRegistration}, TContext>(
        mutationFn,
        mutationOptions,
      );
    };

    引用元

    サンプルアプリケーションの「自動生成されたコードの利用」に記載されているように、自動生成されたカスタムフックを直接利用せずに途中にService層を挟むことで必要に応じてカスタマイズ可能な余地を設けることができます。

    カスタマイズの一例として、サンプルアプリケーションの「データ更新時のキャッシュの扱いについて」に記載があるように、データの更新に成功した際にReact Queryがキャッシュした古いキャッシュデータの破棄などがあります。キャッシュデータの破棄をこのサービス層でおこなうことで、自動生成コードは持たない処理を追加できるかつ、キャッシュデータの破棄に関する処理を集約できます。※3

    以下のコード例では自動生成されたQuery Keysを取得する処理を利用してクエリ成功時にキャッシュを破棄しています。

    【サービス層でキャッシュデータの破棄をおこなうコード例】

    const usePutTodo = () => {
      const queryClient = useQueryClient();
      // Orvalによって自動生成されたカスタムフック(usePutTodoApi)に処理を追加している
      return usePutTodoApi({
        mutation: {
          onSuccess: (_, variables) => resetQueries(queryClient, variables.todoId),
        },
      });
    };
    ~中略~
    const resetQueries = async (queryClient: QueryClient, todoId?: number) => {
      ~中略~
      if (todoId) {
        // Query Keysを取得する処理を利用してクエリ成功時にキャッシュを破棄している
        await queryClient.resetQueries(getGetTodoQueryKey(todoId));
      }
    };
    

    引用元

    サービス層で作成したカスタムフックはそれぞれの画面で利用可能です。
    以下のコード例は取得した情報を表示・編集する画面の一部でtodoIdをもとに情報取得たり入力値(title, description)をバックエンドアプリケーションに送信して情報更新ています。

    【画面でサービス層のカスタムフックを利用するコード例】

    //todoIdをもとに情報の取得している
    const todoQuery = useGetTodo(todoId);
    const todo = todoQuery.data?.data;
    const putTodo = usePutTodo();
    ~中略~
    const onSave = useCallback(async () => {
      ~中略~
        const data = {title, description};
        //入力値(title, description)をバックエンドアプリケーションに送信して情報の更新している
        await putTodo.mutateAsync({todoId, data});
        setIsEdit(false);
    

    引用元

    OpenAPI定義ファイルの管理ルール

    Orvalを利用するにあたって、開発チーム内でOpenAPI定義ファイルをどのように管理していたかも紹介します。

    私達の開発チームではOpenAPI定義ファイルをWebアプリケーションのソースコードと同じリポジトリに入れて管理していました。また、OpenAPI定義ファイルに変更がある場合はかならずOrvalによるコード自動生成とコミットをセットにし、OpenAPI定義ファイルの状態と自動生成されたコードの状態が常に一致する状態にすることをルールとすることでコードの状態を把握しやすくなるのでお勧めです。

    最後に

    冒頭にも書きましたが、React Queryを利用した通信状態の管理とOrvalを利用したコード自動生成の組み合わせは効果的で、開発コストの削減にも役立ちました。また、サンプルアプリケーションは、Reactを使ったWebアプリケーション開発にも親和性があり、周辺ライブラリを使った開発技法も十分に活かすことができます。今後もFintanのReact Nativeに関する記事に注目して、Webアプリケーション開発に活かせるものがあれば紹介します。

     

    ※1: 参照元のサンプルアプリケーションは改善・更新されていくため、本記事作成時点でのリンクを貼っています。参照する際は最新版もご確認ください。

    ※2: 記事公開時点ではReact Queryはv4でTanStack Queryと名前を変えましたが、本記事で取り上げているアプリ開発時はv3が最新だったため、記事内では一貫してReact Queryと表記しています。

    ※3: React Queryのキャッシュについてより理解を深める場合はサンプルアプリケーションの「React Queryの仕組み」を一度読むことをお勧めします。