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


はじめに

今日も今日とて、フロントReact + バックRailsの...

ということで、今回は今更ながら graphql-code-generator の便利さを痛感するの続きとして、登録、削除等のMutationの部分をやっていきます。

※構成は前回と同じです。

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

まずはTODOを追加する仕組みを作っていきます。

追加用のテキストフィールドを用意しました。
ここに値を入力して、エンターで追加処理が実行される仕組みとします。

src/App.tsx
+import { gql, useMutation } from "@apollo/client";

...

+const ADD_TODO = gql`
+  mutation addTodo($name: String!) {
+    addTodo(input: { name: $name }) {
+      todo {
+        id
+        name
+      }
+    }
+  }
+`;

 const App = () => {
   const { loading, data } = useTodosQuery();
+  const [addTodo] = useMutation(ADD_TODO);

...
         </p>
+        <input
+          type="text"
+          onKeyPress={(e) => {
+            if (e.key === "Enter") {
+              addTodo({ variables: { name: e.currentTarget.value } });
+            }
+          }}
+        />
         {loading ? (
...

文字を入力して、エンターで追加処理が実行できる仕組みができました。

エンター押下後、画面をリロードすると、TODOが追加されていることがわかります。

リロードしないと動きがわからないので、ついでに、追加処理に加え、入力欄のクリア、一覧情報の再取得処理をやっておきます。

src/App.tsx
...
-  const { loading, data } = useTodosQuery();
+  const { loading, data, refetch } = useTodosQuery();
-  const [addTodo] = useMutation(ADD_TODO);
+  const [addTodo] = useMutation(ADD_TODO, {
+    update() {
+      refetch();
+    },
+  });

...

           onKeyPress={(e) => {
             if (e.key === "Enter") {
               addTodo({ variables: { name: e.currentTarget.value } });
+              e.currentTarget.value = "";
             }
           }}

特に型関連でエラーは起きませんでした。

もう少し便利になるよう作り込んでみる

何も入力せずに、登録処理を実行し、エラーが出るような仕組みを入れます。

Todoにバリデーションエラーのフィールドを追加して、以下のような値がレスポンスに含まれるようにしました。

"errors":[{"field":"name","error":"blank"}]

また、処理の結果が正常に行われたかのresultフィールド(Boolean)を追加しました。

resulttrueなら一覧をリロード、falseならエラー情報をalertで表示する仕組みを作ってみます。

src/App.tsx
 const ADD_TODO = gql`
   mutation addTodo($name: String!) {
     addTodo(input: { name: $name }) {
       todo {
         id
         name
+        errors {
+          field
+          error
+        }
      }
+      result
    }
  }
`;
src/App.tsx
   const [addTodo] = useMutation(ADD_TODO, {
-    update() {
-      refetch();
-    },
-  });
+    update(
+      _cache,
+      {
+        data: {
+          addTodo: {
+            todo: { errors },
+            result,
+          },
+        },
+      }
+    ) {
+      if (result) {
+        refetch();
+      } else {
+        errors.forEach(({ field, error }) => {
+          alert(`${field} ${error}`);
+        });
+      }
+    },
+  });

errorsの型が宣言されていないので、エラーが発生しました。

型宣言をしてあげます。

type ValidationErrorType = {
  field: string;
  error: string;
};
-errors.forEach(({ field, error }) => {
+errors.forEach(({ field, error }: ValidationErrorType) => {
  alert(`${field} ${error}`);
});

エラーは解消され、バリデーションエラーメッセージがalertで確認できるようになりました。

前回同様、graphql-code-generatorを使って、綺麗にしていこうと思います。

graphql-code-generatorを使う

まずは設定から

queriesとは別にmutations用のディレクトリを作成して、今回作成したクエリは、そちらに格納します。

codegen.yml
-documents: ./graphql/queries/*.graphql
+documents:
+  - ./graphql/mutations/*.graphql
+  - ./graphql/queries/*.graphql
graphql/mutations/add_todo.graphql
mutation addTodo($name: String!) {
  addTodo(input: { name: $name }) {
    todo {
      id
      name
      errors {
        field
        error
      }
    }
    result
  }
}

これでyarn generateを実行すると、src/types.d.tsにTODOを追加する用のuseAddTodoMutationが用意されました。

useAddTodoMutationを使って追加処理を書き直してみます。

-import { useTodosQuery } from "./types.d";
+import { useTodosQuery, useAddTodoMutation } from "./types.d";

+const [addTodo] = useMutation(ADD_TODO, {
+const [addTodo] = useAddTodoMutation({

ん?エラーが起きました。

どうやらdatanullundefinedの可能性があるそうです。

それらを考慮して、少し書き直します。

const [addTodo] = useAddTodoMutation({
  update(_cache, { data }) {
    const result = data?.addTodo?.result || false;
    const errors = data?.addTodo?.todo.errors || [];

    if (result) {
      refetch();
    } else {
      errors.forEach((e) => {
        if (e) alert(`${e.field} ${e.error}`);
      });
    }
  },
});

少し手直しが必要でしたが、クエリと、型宣言の用意が不要になったので、ソース的にはスッキリしました

同様に削除処理を実装してみる

雑ですが、各TODOの隣に、削除ボタンを用意して、削除処理を実行できるようにします。

graphql/mutations/del_todo.graphql
mutation delTodo($id: ID!) {
  delTodo(input: { id: $id }) {
    todo {
      id
    }
  }
}

削除対象のTODOを特定するために、IDを取得する必要があります。

graphql/queries/todos.graphqlでは、TODOのnameだけ取得しているので、idもとるように修正します。

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

yarn generateを実行するとuseDelTodoMutation関数がsrc/types.d.tsに生まれました。

このuseDelTodoMutationを使って、削除ボタンクリック時に削除処理が実行できるようにします。

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


+const [delTodo] = useDelTodoMutation({
+  update() {
+    refetch();
+  }
+});


-{data && data.todos.map(({ name }, i) => <li key={i}>{name}</li>)}
+{data && data.todos.map(({ id, name }, i) => <li key={i}>{name}<button onClick={() => delTodo({ variables: { id } })}>削除</button></li>)}

お手軽に削除処理が実装できました

最後に

一つの画面、コンポーネントで、一覧取得、追加処理、削除処理を実装したので、少しボリュームの大きめなファイルとなってしまったかもしれません。

別のファイルに型定義やクエリ情報を定義して、importでも良いかもしれないですが、人力で且つ、複数人となると、管理が次第に煩雑になったりする可能性があると思います。

これが、ある意味graphql-code-generatorのルールに則って開発をしていると、その問題が解消されるのではないかと思いました。

今回は、かなり小規模な例として、実装したので、導入にあたって障害となる箇所をあまり感じられませんでした。
LGTM なツールと思います