今更ながら graphql-code-generator の便利さを痛感する


はじめに

今日も今日とて、フロントReact + バックRailsのSPA + APIのアプリ開発していたところ

TypeScriptのReact側で、react-apolloの型宣言がめんどくさいと思っていました。

バックエンド側はGraphQLを使用しているので、いろんなところに型宣言をしているようにも感じて、微妙。。

そこでgraphql-code-generatorを使っていろいろ気持ち悪い部分を解消していこうという話をします。

今回の構成

フロントエンド

GitHub

  • React(SPAで)
  • TypeScript
  • create-react-app
  • React Apollo

バックエンド

GitHub

  • Ruby
  • Rails(APIで)
  • GraphQL

※上記2つのリポジトリはこちらのリポジトリから連動させる仕組みとしました。(開発環境として)

とりあえずApolloの公式通りにやってみる

バックエンド側にtodosという、Todoモデルにあるデータを全て取得するAPIを作成しておきました。

これをフロントエンド側で取得し、表示します。

src/App.tsx
+import { gql, useQuery } from "@apollo/client";
 import React from "react";
 import logo from "./logo.svg";
 import "./App.css";

+const TODOS_QUERY = gql`
+  query {
+    todos {
+      name
+   }
+  }
+`;

 const App = () => {
+   const { loading, data } = useQuery(TODOS_QUERY);
+
   return (
     <div className="App">
       <header className="App-header">
         <img src={logo} className="App-logo" alt="logo" />
         <p>
           Edit <code>src/App.tsx</code> and save to reload.
         </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
+        {loading ? (
+          <p>Loading ...</p>
+        ) : (
+          <ul>
+            {data && data.todos.map(({ name }, i) => <li key={i}>{name}</li>)}
+          </ul>
+        )}
       </header>
     </div>
   );
 };

 export default App;

型宣言していないので、エラーが出ましたね。

型宣言してあげます。

用意する型は、Todoモデルの型と、レスポンス値の型です。

レスポンスは{"data":{"todos": []}という値が返るようにしています。

interface Todo {
  name: string;
}

interface TodosData {
  todos: Todo[];
}

TodosDataを以下のように使います。

-  const { loading, data } = useQuery(TODOS_QUERY);
+  const { loading, data } = useQuery<TodosData>(TODOS_QUERY);

エラーなく実行できました。

では、GraphQL Code Generatorが入っていたらどうなるか試してみます。

GraphQL Code Generatorを使う

公式の手順通り、インストールとセットアップ

$ yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
package.json
"scripts": {
  "generate": "graphql-codegen"
}

バックエンド側に用意しているエンドポイントはhttp://localhost:5000/graphqlなので、schemaにこれを使います。

codegen.yml
schema: http://localhost:5000/graphql
documents: ./graphql/queries/*.graphql
generates:
  ./src/types.d.ts:
    plugins:
      - typescript
      - typescript-operations

documentsオプションを使用する場合に、@graphql-codegen/typescript-operationsが必要みたいです。

documentsに指定した場所に、todosのクエリを記載します。

graphql/queries/todos.graphql
query {
  todos {
    name
  }
}

バックエンドを起動してある状態で、用意したgenerateコマンドを実行してみます。

$ yarn generate

src/types.d.tsファイルが生成されました。

src/types.d.ts
...省略...

export type Unnamed_1_QueryVariables = Exact<{ [key: string]: never; }>;


export type Unnamed_1_Query = (
  { __typename?: 'Query' }
  & { todos: Array<(
    { __typename?: 'Todo' }
    & Pick<Todo, 'name'>
  )> }
);

Unnamedとなってしまっているので、クエリに名前をつけて再度実行します。

graphql/queries/todos.graphql
-query {
+query todos {
   todos {
     name
   }
 }

src/types.d.tsファイルにTodosQueryというTypeが定義されました。

これをuseQueryの型に利用してみます。

src/App.tsx
+ import { TodosQuery } from "./types.d";

- const { loading, data } = useQuery<TodosData>(TODOS_QUERY);
+ const { loading, data } = useQuery<TodosQuery>(TODOS_QUERY);

同じように動作が確認できました。

src/App.tsxTodoモデルの型と、レスポンス値の型を定義しなくて良くなりました。

でも、TODOデータを取得する為のクエリをsrc/App.tsxgraphql/queries/todos.graphqlの2箇所に書いているのが気持ち悪いですよね。

todosを取得する為の専用のuseQueryがあれば型もクエリも渡さなく済むのに。。。

@graphql-codegen/typescript-react-apolloを導入する

todosを取得する為の専用のuseQueryがあれば型もクエリも渡さなく済むのに。。。

ということで、この気持ち悪いを解消していきます。

まずはインストール

$ yarn add -D @graphql-codegen/typescript-react-apollo

typescript-react-apolloを追加します。

codegen.yml
 schema: http://localhost:5000/graphql
 documents: ./graphql/queries/*.graphql
 generates:
   ./src/types.d.ts:
     plugins:
       - typescript
       - typescript-operations
+      - typescript-react-apollo

生成コマンドを実行

$ yarn generate

src/types.d.tsuseTodosQueryという関数が生成されたので使ってみます。

src/App.tsx
- import { TodosQuery } from "./types.d";
+ import { useTodosQuery } from "./types.d";

- const { loading, data } = useQuery<TodosQuery>(TODOS_QUERY);
+ const { loading, data } = useTodosQuery();

ちゃんと動きましたね。

GraphQLの便利なところとして、同じAPIでも、必要なフィールドのみを取得することができる特徴があります。

先ほど、todosのデータを取得する際、nameのみ指定し取得、一覧表示のような機能を実現しました。

これを個別に、編集、削除といった機能を実現するには、nameがユニークでない限り、idのようなもので、

todoを特定する必要があります。

別の画面等で、nameに加え、idも必要な場面があった場合、以下のようなクエリを別で作成したくなってきます。

query todos {
  todos {
    id
    name
  }
}

しかし、graphql-code-generatorで生成する関数は全て、src/types.d.tsに入るように設定しています。

ここには既に、nameのみを指定したtodosを取得するクエリの関数が存在しているので、以下のようなファイルを作成し、

yarn generateを実行すると、Not all operations have an unique nameというエラーが発生します。

graphql/queries/todosIncludeId.graphql
query todos {
  todos {
    id
    name
  }
}

queryの右に記載している名前が、ユニークでないといけないってことですね。

graphql/queries/todosIncludeId.graphql
-query todos {
+query todosIncludeId {
   todos {
     id
     name
   }
 }

queryの名前をユニークな名前に変更してみました。

すると、src/types.d.tsuseTodosQueryとは別に、useTodosIncludeIdQuery関数が生成されました。

GraphQLの便利な特性を潰すことなく利用できますね。

参考文献