GraphQL codegen: react-query + graphql-request + hasura
Goal
- React から Hasura で作った GraphQL の API に接続するアプリを作成する
- Hasura へのアクセスは React Query を使用したい
- 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
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;
schema に Hasura を指定する
cf.
- https://www.codedaily.io/tutorials/Setup-Hasura-with-GraphQL-Code-Generator
- https://hasura.io/docs/latest/security/multiple-admin-secrets/
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 } } }]
として指定すれば良さそう
.env
で指定する
Hasura のエンドポイントと admin secret を 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_URL
は undefined
になってしまう
$ 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.
- https://the-guild.dev/graphql/codegen/docs/config-reference/require-field
- https://qiita.com/kohei_abe/items/3aff9ee993fceca63153
動作確認
$ 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 されてないっぽい
.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.json
と src/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.ts
は function graphql()
が export されていた
react-query の設定で codegen できるようにする
cf.
- https://hasura.io/blog/getting-started-with-react-query-and-graphql/
- https://tech.hicustomer.jp/posts/graphql-codegen-react-query/
下記のプラグインを設定すれば良さそう
- typescript
- typescript-operations
- typescript-react-query
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 thelegacyMode
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
);
📝 react-query or @tanstack/react-query
You're looking at the v3 version of react-query. Starting with v4, react-query is now available as @tanstack/react-query
Find the docs at https://tanstack.com/query/latest
cf. https://www.npmjs.com/package/react-query
react-query は v4 から @tanstack/react-query
になったらしいので @tanstack/react-query
を使っておけば良さそう
$ npm i @tanstack/react-query
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
を使ったコードが生成されると書かれているが動作しなかった
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
を使ったコードが出力されたまま
公式ドキュメントの例
Using graphql-request
If you are usinggraphql-request
, you can setfetcher
tographql-request
, and then the generated React Hook will expect you to pass theGraphQLClient
instance (created bygraphql-request
library).
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 を引数に渡すことができない状態
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 bypreset: 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 Generator3.0
milestone aims to provide a unified front-end experience through thepreset: client
, the3.x
versions aim to fully rewrite the core packages of codegen.
今までは Apollo や React-Query などライブラリごとのプラグインを使ってそれぞれのコードを出力していたが、graphql-codegen v3 からは preset: client
(@graphql-codegen/client-preset) を使うことでライブラリごとのコードを出力することなく使える型が出力できるようになったっぽい
cf.
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
手順
-
@graphql-codegen/typescript-react-query
は不要なので uninstall する -
codegen.ts
の編集 - vite + swc 用のプラグインを設定
- codegen
@graphql-codegen/typescript-react-query
を uninstall
1. client-preset はプラグインでクライアント用のコードを出力する必要がなくなるので、このプラグインは不要になる
$ npm uninstall @graphql-codegen/typescript-react-query
codegen.ts
の編集
2. プラグインの指定を削除
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 に設定を追加
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.
📝 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
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
isReactHook: true
true
なら React Hooks useXXXX
形式で出力
Depending on the
isReactHook
property, yourmyFetcher
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>)
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
Hasura に JWT でアクセスする
header に下記のフォーマットで Token を渡せば OK
Authorization: Bearer <JWT>
react-query + graphql-request で Hasura にリクエストを投げる
1. react-query の プロバイダーを設定
キャッシュとかを管理してるらしい
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 を作成
.env
に VITE_HASURA_URL="hasura_graphql_endpoint_url"
を設定しておく
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 にクエリを投げる
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