GraphQL fragment colocation でコンポーネントの保守性を高める

はじめに

【全部俺】 Web フロントエンドエンジニアのメモ Advent Calendar 2022 24 日目の記事です。
urql + GraphQL Code Generator (client-preset) を組み合わせて fragment colocation を実現し、コンポーネントを保守性を高める方法を解説します。
(React 18, urql 3, @graphql-codegen/cli 2.16, @graphql-codegen/client-preset 1.2 を使用しています)

お詫び

執筆途中の記事ですが公開します。情報が正しくない可能性もあります。中途半端な記事となっており大変申し訳ございません。後日書き直しします。

背景

とある GraphQL で通信するプロジェクトでは、 feature パターンでプロジェクトを構成しています。
ドメインごとにディレクトリを切り、その中にコンポーネントディレクトリを作成するパターンです。
bulletproof-react/project-structure.md at 2facb64e827836fcd3ead4c1f7603760b7456619 · alan2207/bulletproof-react
https://github.com/alan2207/bulletproof-react/blob/2facb64e827836fcd3ead4c1f7603760b7456619/docs/project-structure.md
このパターンでは、feature 配下にあるすべてのコンポーネントが特定のドメインへ依存することになります。この特性を活用し、 fragment colocation を行うことで、データ取得に関する関心の分離と依存関係の明確化を行うことができます。
なお、GraphQL クライアントライブラリの 1 つである Relay を使用すると、ライブラリの規約により fragment colocation を行うことができます。一方、今回はプロジェクトの規模が大きくなく、軽量に実装することを目的にあえて urql + GraphQL Code Generator の組み合わせて実装してみました。

GraphQL Code Generator の設定

GraphQL Code Generator と client-preset をインストールします。
npm install @graphql-codegen/cli @graphql-codegen/client-preset
codegen.yml を作成します。
schema: "../graphql/**/*.graphqls"
documents:
  - "src/**/*.{ts,tsx}"
generates:
  ./src/generated/gql/:
    preset: "client"

コンポーネントの実装

例えばとあるブログページに、ブログタイトルコンポーネントと、ブログコンテンツコンポーネントがあった場合、下記のように実装できます。
page.tsx
import { useQuery } from "urql";
import { graphql } from "src/generated/gql";

const PageQuery = graphql(`
  query PageQuery($slug: string!) {
    blogPost(slug: $slug) {
      id
      ...BlogTitleFragment
      ...BlogContentCardFragment
    }
  }
`);

export const ViewerPage = ({ slug }: { slug: string }): JSX.Element => {
  const [result] = useQuery({
    query: PageQuery,
    variables: { slug },
  });

  if (!result.data.blogPost.id) {
    return null;
  }

  return (
    <div>
      <BlogTitle blogPost={result.data.blogPost} />
      <BlogContent blogPost={result.data.blogPost} />
    </div>
  );
};
BlogPostList.tsx
import { graphql, useFragment, FragmentType } from "src/generated/gql";

const BlogPostTitleFragment = graphql(`
  Fragment BlogPostTitleFragment on BlogPost {
    id
    title
  }
`);

export const BlogPostTitle = ({
  blogPost: _blogPost,
}: {
  slug: FragmentType<typeof BlogPostTitleFragment>;
}): JSX.Element => {
  const blogPost = useFragment(BlogPostTitleFragment, blogPost);

  return <h1>{blogPost.title}</h1>;
};
BlogContent.tsx
import { graphql, useFragment, FragmentType } from "src/generated/gql";

const BlogContentFragment = graphql(`
  Fragment BlogContentFragment on BlogPost {
    id
    content
  }
`);

export const BlogContent = ({
  blogPost: _blogPost,
}: {
  slug: FragmentType<typeof BlogContentFragment>;
}): JSX.Element => {
  const blogPost = useFragment(BlogContentFragment, blogPost);

  return <div>{blogPost.content}</div>;
};

メリット

実は useFragment を使用する前の blogPost prop の型では titlecontent が隠蔽されており、取り出すことが出来ません。
useFragment を使って初めて取り出すことができます。 Fragment のスキーマは export していないため、他のコンポーネントから useFragment のパラメータに設定できません。
つまり、Fragment を定義した該当のコンポーネントでしか取り出すことができません。
これが、fragment colocation の最大のメリットです。
  • 変化の激しい時代、スキーマの変更は毎日のように起こりますが(?)、それでも影響範囲が可視化され、安心してスキーマを変更することが出来ます。
  • 親のコンポーネントは子から Fragment をもらって代理アクセスしてそのまま渡すだけなので、子のコンポーネントが求めているデータを知る必要がなくなります。
  • props の型は FragmentType を用いており、スキーマ変更のたびに props の型を書き換える必要がありません。

まとめ

GraphQL fragment colocation の手法を採用したことにより、コンポーネントの保守性を大きく高めることができました。

ホームプロフィール外部リンクのため、別ウインドウで開きますプライバシーポリシー

© 2023 Oishi Takanori / Made with Gatsby.js