Tech & Design LAB

GraphQL Code Generator + React Queryの紹介

#フロントエンド #GraphQL #React Query
author icon
Posted on
tech

こんにちは、エンジニアの尾島(@daikiojm)です。 最近は HiCustomer のオフィスがある五反田周辺の飲食店の入れ替わりが激しく、開店/閉店に一喜一憂しております。 五反田、目黒周辺でおすすめのお店があったら教えてください。

React + Vite で新規プロダクトを開発している話で紹介した新プロダクトの開発では、GraphQL を採用しています。 今回はこの記事では紹介しきれなかった GraphQL Code Generator と React Query の活用について紹介します。

この記事で紹介する内容は次のとおりです。

GraphQL Code Generator + React Query の採用

弊社では GraphQL を採用する以前から REST API のスキーマをSwagger(Open API)で管理し、Swagger Codegenを使って TypeScript の型定義とクライアントを自動生成する運用を行っていました。

この運用は一定ワークしており、型安全なアプリケーションを開発する上では欠かせないものとなっていました。

今回新規で開発しているプロダクトでは GraphQL を採用しましたが、こちらでも Swagger での運用と同様に TypeScript の型定義とクライアントを自動生成する方法を採用したいと考えました。

GraphQL のスキーマから TypeScript の型を生成する方法はいくつか選択肢がありますが、今回はプラグインを使用することでサーバーサイド、フロントエンド共通の型定義を生成できるジェネレーターである、GraphQL Code Generator を使用することにしました。

GraphQL Code Generator がプラグインで対応している GraphQL クライアント(と付随するライブラリの組み合わせ)で代表的なものは以下のとおりです。

この内今回は次の理由から React Query を採用することにしました。

「サーバーデータの状態管理は極力考えずに開発をしたい」については、graphql-request を使用する場合別途サーバーデータの状態管理について考慮する必要があります。この点、Apollo Client、React Query はキャッシュストアを内包しているため、実際に記述するコード量も削減できることが期待できます。(最低限キャッシュキーや、インバリデーションについて考慮する必要はありますが)

「一部の機能において REST API を使用する可能性があること」については、React Query や SWR などのライブラリであれば fetcher を入れ替えることで GraphQL 以外のデータソースにも対応できることができるため、この点において Apollo Client よりも今回の要件にあっていると判断しました。

上記の理由とは直接関係ありませんが、React Query のドキュメントにある以下の記述は Apollo Client との違いとしても興味深い点です。1

Keep in mind that React Query does not support normalized caching. While a vast majority of users do not actually need a normalized cache or even benefit from it as much as they believe they do, there may be very rare circumstances that may warrant it so be sure to check with us first to make sure it’s truly something you need!

改めてこの記事を書くために調べていて見つけたのですが、graphql-codegen-plugin-typescript-swrという GraphQL Code Generator で SWR のクライアントコードを生成するためのプラグインもあるみたいです。(こちらは未検証)

GraphQL Code Generator を使った型/React Query のクライントコード生成

GraphQL Code Generator を使って TypeScript の型定義、React Query のクライアントコードを生成する方法を紹介します。

今回は、フロントエンドとサーバーサイド共通で使うスキーマの TypeScript 型定義、サーバーサイドの resolver の TypeScript 型定義、React Query のクライアントコードを生成することを想定しています。

使用しているプラグインは以下のとおりです。

設定ファイルは次のようになります。

# codegen.yml
overwrite: true
schema: 'schema/**/*.graphql'
generates:
  ./backend/resolvers/types.d.ts:
    plugins:
      - typescript
      - typescript-resolvers
  ./frontend/src/generated/index.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-query
    documents: 'frontend/src/**/*.graphql
    config:
      fetcher: graphql-request
      isReactHook: true
      exposeQueryKeys: true

documents に次のような Query と Mutation が定義されているとします。

# searchCustomer.graphql
query searchCustomer(
  $first: Int
  $last: Int
  $offset: Int
  $after: Cursor
  $before: Cursor
  $orderBy: [CustomersOrderBy!]
) {
  customers(
    first: $first
    last: $last
    offset: $offset
    after: $after
    before: $before
    orderBy: $orderBy
  ) {
    edges {
      cursor
    }
    nodes {
      ... on Customer {
        id
        name
        createdAt
        updatedAt
      }
    }
    totalCount
    pageInfo {
      hasPreviousPage
      hasNextPage
      startCursor
      endCursor
    }
  }
}
# updateCustomer.graphql
mutation updateCustomer($input: UpdateCustomerInput!) {
  updateCustomer(input: $input) {
    customer {
      id
      name
      createdAt
      updatedAt
    }
  }
}

この状態で graphql-codegen を実行する以下のコードが生成されます。

(以下は生成された型定義の部分は省略しています)

// generated/index.ts
import { GraphQLClient } from 'graphql-request'
import {
  useMutation,
  UseMutationOptions,
  useQuery,
  UseQueryOptions,
} from 'react-query'
export type Maybe<T> = T | null
export type Exact<T extends { [key: string]: unknown }> = {
  [K in keyof T]: T[K]
}
export type MakeOptional<T, K extends keyof T> = Omit<T, K> &
  { [SubKey in K]?: Maybe<T[SubKey]> }
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> &
  { [SubKey in K]: Maybe<T[SubKey]> }

function fetcher<TData, TVariables>(
  client: GraphQLClient,
  query: string,
  variables?: TVariables
) {
  return async (): Promise<TData> =>
    client.request<TData, TVariables>(query, variables)
}

設定ファイルで fetcher に graphql-request を使用するように指定しているため、fetcher が graphql-request の client インスタンスを受け取る型が生成されています。

// generated/index.ts
export const useSearchCustomerQuery = <
  TData = SearchCustomerQuery,
  TError = unknown
>(
  client: GraphQLClient,
  variables?: SearchCustomerQueryVariables,
  options?: UseQueryOptions<SearchCustomerQuery, TError, TData>
) =>
  useQuery<SearchCustomerQuery, TError, TData>(
    ['searchCustomer', variables],
    fetcher<SearchCustomerQuery, SearchCustomerQueryVariables>(
      client,
      SearchCustomerDocument,
      variables
    ),
    options
  );
useSearchCustomerQuery.getKey = (variables?: SearchCustomerQueryVariables) => [
  'searchCustomer',
  variables,
];
// generated/index.ts
export const useUpdateCustomerMutation = <TError = unknown, TContext = unknown>(
  client: GraphQLClient,
  options?: UseMutationOptions<
    UpdateCustomerMutation,
    TError,
    UpdateCustomerMutationVariables,
    TContext
  >
) =>
  useMutation<
    UpdateCustomerMutation,
    TError,
    UpdateCustomerMutationVariables,
    TContext
  >(
    (variables?: UpdateCustomerMutationVariables) =>
      fetcher<UpdateCustomerMutation, UpdateCustomerMutationVariables>(
        client,
        UpdateCustomerDocument,
        variables
      )(),
    options
  );

設定ファイルで isReactHook を true を指定しているため、生成されるクライアントコードのファンクション名が useXXX となっています。

Custom Hooks の実装例

GraphQL Code Generator で生成した React Query のクライアントを実際に使用する例を紹介します。それぞれ Custom Hooks として実装する例としています。

GraphQL Code Generator で生成した React Query のクライアントは react-query の client を引数に取るようになっています。 以下は Recoil の store に格納されている API Token をリクエストヘッダーに付加した状態の client を提供する Custom Hooks の例です。

// useConfiguredGraphQLClient.ts
import { GraphQLClient } from 'graphql-request';
import { useRecoilValue } from 'recoil';

import { idTokenState } from '@/store/idToken';

const endpoint = process.env.VITE_API_ENDPOINT;

const buildHeaders = (idToken?: string) => {
  if (!idToken) {
    return {};
  }

  return {
    headers: {
      Authorization: `Bearer ${idToken}`,
    },
  };
};

export function useConfiguredGraphQLClient(): {
  createGraphQLClient: () => GraphQLClient;
  createGraphQLAuthClient: () => GraphQLClient;
} {
  const idToken = useRecoilValue(idTokenState);

  const createGraphQLClient = () => {
    return new GraphQLClient(endpoint);
  };

  const createGraphQLAuthClient = () => {
    return new GraphQLClient(endpoint, {
      ...buildHeaders(idToken),
    });
  };

  return {
    createGraphQLClient,
    createGraphQLAuthClient,
  };
}

以下は useSearchCustomerQuery をラップした Custom Hooks の例です。

// useCustomersQuery.ts
import { UseQueryResult } from 'react-query';

import {
  SearchCustomerQuery,
  SearchCustomerQueryVariables,
  useSearchCustomerQuery,
} from '@/generated';
import { useConfiguredGraphQLClient } from '@/hooks/useConfiguredGraphQLClient';
import { useSnackbar } from '@/hooks/useSnackbar';

const snackbarMessages = {
  error: `エラーが発生しました`,
} as const;

export function useCustomersQuery(
  variables: SearchCustomerQueryVariables
): UseQueryResult<SearchCustomerQuery, unknown> {
  const { createGraphQLAuthClient } = useConfiguredGraphQLClient();
  const { showError } = useSnackbar();

  const result = useSearchCustomerQuery(createGraphQLAuthClient(), variables, {
    onError: (e: unknown) => {
      showError(snackbarMessages.error);
    },
  });

  return result;
}

今回の例はサンプルなので省略していますが、上記の他詳細なキャッシュキーの指定や Query、Mutaion ごとのキャッシュルールの指定などを行うことになるかと思います。

以下は useUpdateCustomerMutation をラップした Custom Hooks の例です。

// useUpdateCustomerMutation.ts
import { useQueryClient } from 'react-query';

import {
  UpdateCustomerMutation,
  UpdateCustomerMutationVariables,
  useUpdateCustomerMutation as useUpdateCustomerMutationGenerated,
} from '@/generated';
import { useConfiguredGraphQLClient } from '@/hooks/useConfiguredGraphQLClient';
import { useSnackbar } from '@/hooks/useSnackbar';

const invalidateQueryKey = 'searchCustomer';
const snackbarMessages = {
  success: `カスタマーを更新しました`,
  error: `エラーが発生しました`,
} as const;

export function useUpdateCustomerMutation(): {
  mutateUpdateCustomer: (
    variables: UpdateCustomerMutationVariables
  ) => Promise<UpdateCustomerMutation>;
  isLoadingUpdateCustomer: boolean;
} {
  const queryClient = useQueryClient();
  const { createGraphQLAuthClient } = useConfiguredGraphQLClient();
  const { showSuccess, showError } = useSnackbar();

  const { mutateAsync, isLoading: isLoadingUpdateCustomer } =
    useUpdateCustomerMutationGenerated(createGraphQLAuthClient(), {
      onSettled: () => {
        queryClient.invalidateQueries(invalidateQueryKey, {
          refetchActive: true,
        });
      },
      onError: (e: unknown) => {
        showError(snackbarMessages.error);
      },
      onSuccess: () => {
        showSuccess(snackbarMessages.success);
      },
    });

  const mutateUpdateCustomer = async (
    variables: UpdateCustomerMutationVariables
  ) =>
    await mutateAsync({
      input: {
        id: variables.input.id,
        patch: variables.input.patch,
      },
    });

  return {
    mutateUpdateCustomer,
    isLoadingUpdateCustomer,
  };
}

おわりに

GraphQL Code Generator と React Query の活用について紹介紹介しました。GraphQL スキーマの型に加えてクライアントコードの生成まで行えるため、GraphQL の柔軟な Query に加え TypeScript の型安全性を生かした開発ができるため非常に便利です。

弊社では GraphQL を本格的に採用するプロダクトは今回が初めてであり、サーバーサイド、フロントエンドともに環境整備は試行錯誤をしている段階です。 上記のような環境で開発すことに興味がある方や、「もっとこうやったら良くなるはず!」といったアイデアがある方がいればHiCustomer 採用ページからお気軽にお声掛けください。

author icon
エンジニア

常に変化を求めています。Node.js が好きで、HiCustomer のプロダクトチームではフロントエンドを担当しています。 趣味は slack に治安の悪い絵文字を追加することです。