Open12

GraphQL codegen: react-query + graphql-request + hasura

KiKiKi-KiKiKiKiKi-KiKi

Goal

  1. React から Hasura で作った GraphQL の API に接続するアプリを作成する
  2. Hasura へのアクセスは React Query を使用したい
  3. GraphQL の型を codegen で生成する

React app

$ npm create vite@latest
Select a framework: › React
Select a variant: › TypeScript + SWC

GraphQL 関連のパッケージをインストール

$ npm i graphql
$ npm i -D @graphql-codegen/cli

環境

  • React: 18.2.0
  • TypeScript: 5.0.2
  • graphql: 16.6.0
  • @graphql-codegen/cli: 3.3.0
KiKiKi-KiKiKiKiKi-KiKi

codegen config

npx graphql-codegen init で対話式で設定ファイルが作成できる
📝 npm run graphql-codegen init, npx run graphql-codegen init はエラーになった

$ npx graphql-codegen init
 What type of application are you building?: Application built with React
? Where is your schema?: http://localhost:4000/graphql
? Where are your operations and fragments?: src/**/*.graphql
? Where to write the output: src/gql/
? Do you want to generate an introspection file?: Yes
? How to name the config file?: codegen.ts
? What script in package.json should run the codegen?: codegen
etching latest versions of selected plugins...
    Config file generated at codegen.ts
      $ npm install
    To install the plugins.
      $ npm run codegen
    To run GraphQL Code Generator.
# 必要なパッケージをインストール
$ npm install

codegen.ts が作られ package.json に必要なパッケージと生成用のスクリプトが追加される
📝 Where to write the output? は最後が / でないと生成時にエラーになる => ✖ [client-preset] target output should be a directory, ex: "src/gql/"

codegen.ts

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  schema: "http://localhost:4000/graphql",
  documents: "src/**/*.graphql",
  generates: {
    "src/gql/": {
      preset: "client",
      plugins: []
    },
    "./graphql.schema.json": {
      plugins: ["introspection"]
    }
  }
};

export default config;
KiKiKi-KiKiKiKiKi-KiKi

schema に Hasura を指定する

cf.

codegen.ts

const config: CodegenConfig = {
  overwrite: true,
- schema: 'http://localhost:4000/graphql',
+ schema: [
+   {
+     'http://localhost:4000/v1/graphql': {
+       headers: {
+         'x-hasura-admin-secret': 'admin_secret'
+       }
+     }
+   }
+ ], 
  documents: 'src/**/*.graphql',
  // …
}

shema に [{ HASURA_URL: { headers: { "x-hasura-admin-secret": ADMIN_SECRET } } }] として指定すれば良さそう


Hasura のエンドポイントと admin secret を .env で指定する

codegen.ts に直接 sectet などが書かれているのあまり良くないので、.env から設定できるようにしたい

.env

HASURA_URL=https://hasura.XXXXX.dev/v1/graphql
HASURA_GRAPHQL_ADMIN_SECRET=your_secret

codegen.ts

const config: CodegenConfig = {
  overwrite: true,
  schema: [
    {
-     'http://localhost:4000/v1/graphql': {
+     [process.env.HASURA_URL ?? "http://localhost:4000/v1/graphql"]: {
        headers: {
-        'x-hasura-admin-secret': 'admin_secret'
+        'x-hasura-admin-secret': process.env.HASURA_GRAPHQL_ADMIN_SECRET ?? '',
        },
      },
    },
  ],
  documents: 'src/**/*.graphql',
  // …
}

.env を使うオプションを追加

そのままでは process.env.HASURA_URLundefined になってしまう

$ npm run codegen --check
> graphql-codegen@0.0.0 codegen
> graphql-codegen --config codegen.ts
✔ Parse Configuration
⚠ Generate outputs
  ❯ Generate to src/gql
✖
      Failed to load schema from http://localhost:4000/v1/gr…

npm run codegen --check を実行すると .env の内容が読み込まれていないことが確認できる

--require dotenv/config オプションを設定する

--require dotenv/config オプション付きで実行すれば dotenv 経由で .env の内容が使えるようになる
📝 vite + react 環境に dotenv が含まれているので別途 npm install する必要はなかった

package.json

"scripts": {
- "codegen": "graphql-codegen --config codegen.ts"
+ "codegen": "graphql-codegen --require dotenv/config --config codegen.ts"
},

cf.

動作確認

$ npm run codegen --check
> graphql-codegen@0.0.0 codegen
> graphql-codegen --require dotenv/config --config codegen.ts

✔ Parse Configuration
⚠ Generate outputs
  ❯ Generate to src/gql/
    ✔ Load GraphQL schemas
    ✖
      Unable to find any GraphQL type definitions for the fo…
      - src/**/*.graphql
    ◼ Generate
  ❯ Generate to ./graphql.schema.json
    ✔ Load GraphQL schemas
    ✖
      Unable to find any GraphQL type definitions for the fo…
      - src/**/*.graphql
    ◼ Generate

schema は読み込めているが document に指定している 'src/**/*.graphql' が何もないので何も generate されてないっぽい

KiKiKi-KiKiKiKiKi-KiKi

適当な .graphql ファイルを作成して codegen する

src/**/*.graphql が存在しないと graphql.schema.json も生成できないようなので適当な query ファイルを作成する
Hasura のコンソールから適当なクエリを作ってコピペすればOK

./src/gql/queries/getSelf.graphql

query GetSelf($email: String!) {
  users(email: $mail) {
    id,
    email,
    displayName
  }
}

codegen

1つでも .graphql ファイルが存在すれば codegen が成功した

$ npm run codegen
> graphql-codegen@0.0.0 codegen
> graphql-codegen --require dotenv/config --config codegen.ts

✔ Parse Configuration
✔ Generate outputs

graphql.schema.jsonsrc/gql/ ディレクトリ内に型ファイルが生成される

|- graphql.schema.json
|- /src/gql/
    |- fragment-masking.ts
    |- graphql.ts
    |- gql.ts
    |- index.ts

/src/gql/index.ts

export * from "./fragment-masking";
export * from "./gql";

fragment-masking.ts が型ファイルで、gql.tsfunction graphql() が export されていた

KiKiKi-KiKiKiKiKi-KiKi

react-query の設定で codegen できるようにする

cf.

下記のプラグインを設定すれば良さそう

This plugin generates React-Query Hooks with TypeScript typings.
It extends the basic TypeScript plugins: @graphql-codegen/typescript, @graphql-codegen/typescript-operations - and thus shares a similar configuration.

If you are using the react-query package instead of the @tanstack/react-query package in your project, please set the legacyMode option to true.

cf. https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-react-query

@graphql-codegen/typescript-react-query だけインストールすれば OK

$ npm i -D @graphql-codegen/typescript-react-query

codegen

codegen.ts にプラグインの設定を追加

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  schema: [
    {
      [process.env.HASURA_URL ?? 'http://localhost:4000/v1/graphql']: {
        headers: {
          'x-hasura-admin-secret':
            process.env.HASURA_GRAPHQL_ADMIN_SECRET ?? '',
        },
      },
    },
  ],
  documents: 'src/**/*.graphql',
  generates: {
    'src/types/gql/': {
      preset: 'client',
-      plugins: [],
+     plugins: [
+       'typescript',
+       'typescript-operations',
+       'typescript-react-query',
+     ],
    },
    './graphql.schema.json': {
      plugins: ['introspection'],
    },
  },
};

export default config;

これで codegen すると graphql.ts が react-query を使用したコードで出力される

$ npm run codegen

graphql.ts に fetcher 関数と react-query の useQuery を使った関数が生成されている

// graphql.ts
function fetcher<TData, TVariables>(endpoint: string, requestInit: RequestInit, query: string, variables?: TVariables) {
  return async (): Promise<TData> => {
    const res = await fetch(endpoint, {
      method: 'POST',
      ...requestInit,
      body: JSON.stringify({ query, variables }),
    });
    const json = await res.json();
    if (json.errors) {
      const { message } = json.errors[0];
      throw new Error(message);
    }
    return json.data;
  }
}

// 略 …

// query GetSelf に対応する関数が生成されている
export const useGetSelfQuery = <
      TData = GetSelfQuery,
      TError = unknown
    >(
      dataSource: { endpoint: string, fetchParams?: RequestInit },
      variables: GetSelfQueryVariables,
      options?: UseQueryOptions<GetSelfQuery, TError, TData>
    ) =>
    useQuery<GetSelfQuery, TError, TData>(
      ['GetSelf', variables],
      fetcher<GetSelfQuery, GetSelfQueryVariables>(dataSource.endpoint, dataSource.fetchParams || {}, GetSelfDocument, variables),
      options
    );
KiKiKi-KiKiKiKiKi-KiKi

graphql-request

graphql-request
Minimal GraphQL client supporting Node and browsers for scripts or simple apps

GraphQL のエンドポイントが Hasura なので react-query の fetcher に fetch を使って毎回 header などを指定するより fetcher を client にしてしまった方が良さそうだったので採用することにした

🐞 graphql-request のオプションが効かない

ライブラリ・プラグインのバージョン

  • tanstack/react-query: 4.29.3
  • graphql-request: 6.0.0
  • @graphql-codegen/typescript-react-query: 4.1.0

@graphql-codegen/typescript-react-queryドキュメントfetcher オプションに graphql-request を指定すると GraphQLClient を使ったコードが生成されると書かれているが動作しなかった

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  schema: [
    {
      [process.env.HASURA_URL ?? 'http://localhost:4000/v1/graphql']: {
        headers: {
          'x-hasura-admin-secret':
            process.env.HASURA_GRAPHQL_ADMIN_SECRET ?? '',
        },
      },
    },
  ],
  documents: 'src/**/*.graphql',
  generates: {
    'src/types/gql/': {
      preset: 'client',
      plugins: [
        'typescript',
        'typescript-operations',
        'typescript-react-query',
      ],
+     config: {
+       fetcher: 'graphql-request',
+     },
    },
    './graphql.schema.json': {
      plugins: ['introspection'],
    },
  },
};

export default config;
$ npm run codegen

👇
出力されるファイルに変化なし。
fetch を使ったコードが出力されたまま

公式ドキュメントの例

cf. https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-react-query#using-graphql-request

Using graphql-request
If you are using graphql-request, you can set fetcher to graphql-request, and then the generated React Hook will expect you to pass the GraphQLClient instance (created by graphql-request library).

codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli'
 
const config: CodegenConfig = {
  schema: 'MY_SCHEMA_PATH',
  documents: './src/**/*.graphql',
  generates: {
    './generates.ts': {
      plugins: ['typescript', 'typescript-operations', 'typescript-react-query'],
      config: {
        fetcher: 'graphql-request'
      }
    }
  }
}
export default config

And the, while using, provide your client instance:

import { useMyQuery } from './generated'
import { client } from './my-graphql-request-client'
 
export const MyComponent = () => {
  const { status, data, error, isFetching } = useMyQuery(client, {})
}

fetch を使った useMyQuery が出力されているので graphql-request の client を引数に渡すことができない状態

KiKiKi-KiKiKiKiKi-KiKi

client-preset

A unified configuration and package for all GraphQL clients

Most of the existing client-side plugins (typescript-react-apollo, typescript-react-query, etc) rely on the generation of hooks or SDKs that wrap the underlying GraphQL Client in a type-safe way.

However, the generation of hooks or SDK code brings many downsides:

  • an unnecessary increase of the final bundle size
  • misalignment between the generated hooks signature and the underlying GraphQL Client
  • inconsistencies of configuration options and preset compatibility across packages (ex: near-operation-file compatibility)
    To make GraphQL code generation great and simple, the v3 version will introduce two major changes:
  • a new unique preset for all GraphQL clients, which include better developer experience, smaller bundle size, stronger typings, and easier-to-follow best practices
  • a TypeScript-first configuration file that will allow configuration autocompletion

Thanks to work made to integrate TypeDocumentNode (the underlying plugin used by preset: client) with most of the popular GraphQL clients, you no longer need hooks or SDK, simple GraphQL documents works!

We believe that the preset: client approach is the way to get the best of TypeScript and GraphQL by:

  • reducing the size of the generated bundle
  • only the graphql() function needs to be imported (no type, hooks, document imports)
  • removing layers between your application and your chosen GraphQL Client
  • providing stronger typings that will stay aligned with your chosen GraphQL Client
  • offering you the best component isolation design by leveraging Fragment Masking

We aim for GraphQL Code Generator 3.0’s client preset to become the official way to generate GraphQL Types for front-end use cases, replacing all existing hook and SDK-based plugins.
Finally, while the GraphQL Code Generator 3.0 milestone aims to provide a unified front-end experience through the preset: client, the 3.x versions aim to fully rewrite the core packages of codegen.

cf. GraphQL Code Generator v4 Roadmap

今までは Apollo や React-Query などライブラリごとのプラグインを使ってそれぞれのコードを出力していたが、graphql-codegen v3 からは preset: client (@graphql-codegen/client-preset) を使うことでライブラリごとのコードを出力することなく使える型が出力できるようになったっぽい

cf.
https://github.com/dotansimha/graphql-code-generator/issues/8296
https://the-guild.dev/graphql/codegen/plugins/presets/preset-client
https://the-guild.dev/graphql/codegen/docs/guides/react-vue
https://zenn.dev/mh4gf/articles/graphql-codegen-client-preset

KiKiKi-KiKiKiKiKi-KiKi

client-preset を使った設定

環境

  • @graphql-codegen/cli 3.3.0
  • @graphql-codegen/client-preset 3.0.0
  • graphql 16.6.0
  • @tanstack/react-query 4.29.3
  • graphql-request 6.0.0

手順

  1. @graphql-codegen/typescript-react-query は不要なので uninstall する
  2. codegen.ts の編集
  3. vite + swc 用のプラグインを設定
  4. codegen

1. @graphql-codegen/typescript-react-query を uninstall

client-preset はプラグインでクライアント用のコードを出力する必要がなくなるので、このプラグインは不要になる

$ npm uninstall @graphql-codegen/typescript-react-query

2. codegen.ts の編集

プラグインの指定を削除

codegen.ts
const config: CodegenConfig = {
  overwrite: true,
  schema: [ 略 ],
  documents: 'src/**/*.graphql',
  generates: {
    'src/types/gql/': {
      preset: 'client',
-     plugins: [
-       'typescript',
-       'typescript-operations',
-       'typescript-react-query',
-     ],
+     plugins: [],
-     config: {
-       fetcher: 'graphql-request',
-     },
+     config: {},
    },
    './graphql.schema.json': {
      plugins: ['introspection'],
    },
  },
};

3. vite + swc 用のプラグインを設定

$ npm i -D @graphql-codegen/client-preset-swc-plugin

vite.config に設定を追加

cite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';

export default defineConfig({
- plugins: [react()], 
+ plugins: [
+   react({
+     plugins: [
+       [
+         '@graphql-codegen/client-preset-swc-plugin',
+         {
+           artifactDirectory: './src/gql',
+           gqlTagName: 'graphql',
+         },
+       ],
+     ],
+   }),
+ ],
});

4. codegen

$ npm run codegen                                   
> graphql-codegen@0.0.0 codegen
> graphql-codegen --require dotenv/config --config codegen.ts
✔ Parse Configuration
✔ Generate outputs

cf.

KiKiKi-KiKiKiKiKi-KiKi

📝 preset-client を使わない方法なら react-query + graphql-request のコードを生成できる

⚠ codegen v4 以降はうまく動作しなくなる可能性あり

$ npm i graphql @tanstack/react-query graphql-request
$ npm i -D @graphql-codegen/cli @graphql-codegen/introspection @graphql-codegen/typescript-react-query
codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  schema: [
    {
      [process.env.HASURA_URL ?? 'http://localhost:4000/v1/graphql']: {
        headers: {
          'x-hasura-admin-secret':
            process.env.HASURA_GRAPHQL_ADMIN_SECRET ?? '',
        },
      },
    },
  ],
  documents: 'src/**/*.graphql',
  generates: {
    // preset-client を使わない場合は出力はファイル名で指定する
    'src/types/generates.ts': {
      plugins: ['typescript', 'typescript-operations', 'typescript-react-query'],
      config: {
        fetcher: 'graphql-request',
        isReactHook: true,
        exposeQueryKeys: true
      }
    },
    './graphql.schema.json': {
      plugins: ['introspection'],
    },
  },
};

export default config;
$ npm run codegen

👇

./src/types/generates.ts に react-query と graphql-request のクライアントを使ったカスタムフックが出力される


Config options

https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-react-query

isReactHook: true

true なら React Hooks useXXXX 形式で出力

Depending on the isReactHook property, your myFetcher should be in the following signature:

  • isReactHook: false
    type MyFetcher<TData, TVariables> = (operation: string, variables?: TVariables, options?: RequestInit['headers']): (() => Promise<TData>)
    
  • isReactHook: true
     type MyFetcher<TData, TVariables> = (operation: string, options?: RequestInit['headers']): ((variables?: TVariables) => Promise<TData>)
    

cf. https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-react-query#usage-example-isreacthook-true

exposeQueryKeys: true

For each generate query hook adds getKey(variables: QueryVariables) function. Useful for cache updates. If addInfiniteQuery is true, it will also add a getKey function to each infinite query.

const query = useUserDetailsQuery(...)
const key = useUserDetailsQuery.getKey({ id: theUsersId })
// use key in a cache update after a mutation

cf. https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-react-query#exposequerykeys

KiKiKi-KiKiKiKiKi-KiKi

react-query + graphql-request で Hasura にリクエストを投げる

1. react-query の プロバイダーを設定

キャッシュとかを管理してるらしい

App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <h1>My App</h1>
      <MyComponent />
    </QueryClientProvider>
  );
}

export { App };

2. GraphQLClient を作成

.envVITE_HASURA_URL="hasura_graphql_endpoint_url" を設定しておく

graphqlClient.ts
import { GraphQLClient } from 'graphql-request';
const endpoint = import.meta.env.VITE_HASURA_URL ?? '';

export const getGraphQLClient = (token?: string) => {
  const headers = token ? { authorization: `Bearer ${token} } : undefined;
  const client = new GraphQLClient(endpoint, {
    headers,
  });

  return client;
};

3. React Query + GraphQLClient (graphql-request) で Haura にクエリを投げる

MyComponent.tsx
import { useQuery } from '@tanstack/react-query';
// codegen で生成された {QueryName}Document を使用する
import { GetUserDocument } from './gql/graphql';
import { useAuth } from './hooks/useAuth';
import { getGraphQLClient } from './graphqlClient';

function MyComponent(): JSX.Element {
  const { token, user } = useAuth();
  const client = getGraphQLClient(token);
  const { data, isLoading, isError, error } = useQuery(
    [`getUser`, user.email],
    ({ queryKey }) => client.request(GetUserDocument, { email: queryKey[1] ?? '' }),
  );

  if (isLoading) { return <div>Loafing...</div> }
  if (isError) {
    console.log({error});
    return <div>Error</div>
  }
  return {
    <div>
      User: { data ? <UserInfo {…data} /> : <span>No User</span> }
    </div>
  }
}

cf

https://tanstack.com/query/v4/docs/react/overview
https://www.npmjs.com/package/graphql-request
https://zenn.dev/eringiv3/articles/56f2b9f90a0632