🌊

gql-tag-operations-preset を使った GraphQL の Fragment Colocation

2022/03/12に公開1

普段 GraphQL を使ったフロントエンド開発をしています。最近 GraphQL Code Generator の設定を見直しており、いくつかの plugin や preset を採用したり削除したのですが、その中でも gql-tag-operations-preset がとても良かったので、もともとの構成と gql-tag-operations-preset を導入した後の構成を比較して記事に残しておきます。

また、 gql-tag-operations-preset を導入することで、 Relay 以外の GraphQL Client でも Fragment Colocation が扱いやすくなります。

私は Apollo ユーザーなので Relay の Fragment Colocation を原則とした開発体験を羨ましく思っていたのですが、 Apollo でも少し近づいた開発体験を手に入れることができました。

https://www.graphql-code-generator.com/plugins/gql-tag-operations-preset

gql-tag-operations-preset を導入する前の開発

gql-tag-operations-preset を導入する前は、主に typed-document-nodenear-operation-file-preset を使った、 GraphQL ファイルにクエリを記述し、同じ階層に TypedDocumentNode が生成されたファイルを置く構成をとっていました。
これを実際に使用するコンポーネントと併せて同じディレクトリに入れておくことで、使わなくなったらディレクトリごと削除する運用にしていました。

└── Books
    ├── Books.tsx
    ├── books.generated.ts
    └── books.graphql

そこまで不満はなかったのですが、やはりクエリとコンポーネントはできるだけ近くにまとめておきたいとは思っていました。同じファイルにも記述できるのですが、その場合はただのテンプレート文字列でしかなく、型の恩恵を得られないのでクエリはコンポーネントとは別ファイルで記述するようにしていました。

gql-tag-operations-preset を導入した後の開発

インストール方法など、基本的にドキュメント通りなのでドキュメントを参考にしてください。

https://www.graphql-code-generator.com/plugins/gql-tag-operations-preset

このようにコンポーネントと同じファイルに書き、型もしっかりと効いていることがわかります。

設定ファイル

gql-tag-operations-preset を導入する前と比べて、使用するパッケージも少なくなったので、GraphQL Code Generator の設定ファイルの記述量も少なくなりました。

gql-tag-operations-preset 導入前

schema: introspection.json
documents: './**/*.graphql'
generates:
  src/types/graphql.tsx:
    plugins:
      - typescript
  ./:
    preset: near-operation-file
    presetConfig:
      extension: .ts
      baseTypesPath: src/types/graphql
    plugins:
      - typescript-operations
      - typed-document-node

gql-tag-operations-preset 導入後

schema: introspection.json
documents: 'src/**/*.tsx'
generates:
  src/gql/:
    preset: gql-tag-operations-preset

便宜上簡略化しています

Fragment Colocation

Fragment Colocation についてはこちらの記事がとてもわかりやすいです。

import { useFragment, FragmentType, gql } from "../../gql";

const BooksQuery = gql(/* GraphQL */ `
  query BooksQuery {
    books {
      id
      ...BookFragment
    }
  }
`);

export const Books: VFC = () => {
  const { data } = useQuery(BooksQuery);

  return (
    <ul>
      {data.books.map((book) => (
        <li key={book.id}>
          <Book book={book} />
        </li>
      ))}
    </ul>
  );
};

const BookFragment = gql(/* GraphQL */ `
  fragment BookFragment on Book {
    title
    author
  }
`);

type Props = {
  book: FragmentType<typeof BookFragment>;
};

export const Book: VFC<Props> = (props) => {
  const book = useFragment(BookFragment, props.book);

  return (
    <>
      <p>{book.title}</p>
      <p>{book.author}</p>
    </>
  );
};

Relay ライクな useFragment もあり、 Fragment Colocation が扱いやすくなりました。

また fragmentMasking オプションを true することで、各コンポーネントはそのフラグメントによって記述されたデータ依存関係にのみアクセスすることができます。

Books コンポーネントでは BookFragment の値にはアクセスできなく、 Book コンポーネントでは BookFragment で定義された値にしかアクセスできなくなっていることがわかります。

はまったポイント

gql 関数を Apollo などの他ライブラリのものから使える augmentedModuleName オプションがあるのですが、これと fragmentMasking オプションを併用すると、 fragment の定義を読み込めずにサーバーがクエリのリクエストを処理できないエラーが発生しました。

参考

GitHubで編集を提案

Discussion

at_sushiat_sushi

以下だとなぜ駄目なのでしょうか?

export const Book: VFC<Props> = (props) => {

  return (
    <>
      <p>{props.book.title}</p>
      <p>{props.book.author}</p>
    </>
  );
};