Gemcook Tech Blog
🐧

GraphQLのFragment Colocationのススメ

2022/05/08に公開

はじめに

この記事では GraphQL の Fragment Colocation という考え方を紹介します。
REST を使用していた頃から不満に感じていたものの、解決が難しいと感じていた課題が Fragment Colocation で見事に解決されたため GraphQL を使用している場合必ず知っておいた方が良い概念だなと思います。

また、前提としてこの記事では Apollo(もしくは Urql 等のデータフェッチライブラリ)を使用していることを想定しており、サンプルコードは React で記述しています。
Relay を使用している場合フェッチデータの宣言をデータを使用するコンポーネントで記述することが強制されるため、自然と Fragment Colocation を使用した実装になります。

この記事で書くこと

  • Fragment Colocation の紹介・基本概念の説明

この記事で書かないこと

  • Apollo, Urql の使用方法
  • Fragment Colocation を使用する時の細かい設計指針(自動生成, 命名規則 ...etc)

既存の問題点

Fragment Colocation を使用していない場合、フェッチデータを各ページのトップ層もしくはグローバルなファイルで管理することになるでしょう。その場合どのような問題があるかを見ていきます。
以下のサンプルでは Apollo を使用していますが、他のライブラリを使用していても基本的な実装は同じです。

SomePage.tsx
import { gql } from '@apollo/client';

// 親コンポーネントが全てのフェッチデータを管理している
const SOME_QUERY = gql`
  query somePageQuery {
    user {
      name
      friends {
        id
        name
      }
    }
  }
`

export const SomePage: React.FC = () => {
  const { data } = useQuery(SOME_QUERY);

  if(!data?.user) return <div>not found<div/>;

  return (
    <>
      <div>
        <p>{data.user.name}</p>
      </div>
      <div>
        {/* Friendを表示する共通コンポーネント */}
        {friends.map(friend => (
          <Friend key={friend.id} friend={friend} />
        ))}
      </div>
    </>
  )
}
Friend.tsx
export const Friend: React.FC<{ friend: Friend }> = ({ friend }) => {
  return (
    <>
      <p>{friend.name}</p>
    </>
  )
}

この例は一見何の問題もなさそうに見えますが「子コンポーネント」に表示するデータを「親コンポーネント」が知っていなければならない、というちょっとした気持ち悪さがあります。
この気持ち悪さが負債として顕在化するのは「子コンポーネント」で必要なデータが変化した時です。
例えば Friend コンポーネントで現在は使用していない age というフィールドが必要になった時、Friend コンポーネントと親コンポーネントを書き換える必要があります。

Friend.tsx
 export const Friend: React.FC<{ friend: Friend }> = ({ friend }) => {
   return (
     <>
       <p>{friend.name}</p>
+      <p>{friend.age}</p>
    </>
   )
 }
SomePage.tsx
 import { gql } from '@apollo/client';

 const SOME_QUERY = gql`
   query somePageQuery {
     user {
       name
       friends {
         id
         name
+        age
       }
     }
   }
 `

 // ...PageComponent

これで追加が完了しました。
しかし、この「2 箇所書き換える」という行為は往々にして負債を生みます。
追加する場合は型が実装ミスを指摘してくれますが、逆にフィールドが不要になり削除する場合は型エラーで検知できないため「使用していない値が宣言されている」という状況が発生してしまいます。
この状況に陥るとオーバーフェッチが発生する上に、他に使用している箇所があるかもしれず開発時に余計な思考リソースが必要となり保守性が下がります。
データを追加したいときにどこに追加を記述すればいいかが直感的にわからず、わざわざ探しに行く必要があるという問題もあります。

これを解決できるのが Fragment Colocation です。

Fragment Colocation

colocate とはそもそも「同じ場所に配置する」という意味です。
つまりコンポーネントとフェッチデータを同じ場所で宣言すれば、1 箇所の記述で綺麗にまとまります。
これを可能にするのが GraphQL のフラグメントの機能です。
フラグメントを使用することで必要なフィールドの指定を再利用可能な形で宣言できます。
例を見た方がわかりやすいと思うため、先ほどのサンプルを元に Fragment Colocation を実践します。

まずは Friend コンポーネントにフラグメントを記述します。

Friend.tsx
import { gql } from '@apollo/client';

export const FRIEND_FIELDS = gql`
  fragment FriendFields on Friend {
    name
  }
`

export const Friend: React.FC<{ friend: FriendFields }> = ({ friend }) => {
  return (
    <>
      <p>{friend.name}</p>
    </>
  )
}

次に親コンポーネントでフラグメントを展開して query を宣言します。
フラグメントは JS のように展開する構文が使用できます。

SomePage.tsx
import { gql } from '@apollo/client';
import { FRIEND_FIELDS } from './Friend';

const SOME_QUERY = gql`
  ${FRIEND_FIELDS}
  query somePageQuery {
    user {
      name
      friends {
        id
        ...friendFields
      }
    }
  }
`;

export const SomePage: React.FC = () => {
  const { data } = useQuery(SOME_QUERY);

  if(!data?.user) return <div>not found<div/>;

  return (
    <>
      <div>
        <p>{data.user.name}</p>
      </div>
      <div>
        {/* Friendを表示する共通コンポーネント */}
        {friends.map(friend => (
          <Friend key={friend.id} friend={friend} />
        ))}
      </div>
    </>
  )
}

id は map 時に使用しているため親コンポーネントの中に残っていますが、これで親コンポーネントから Friend のデータ定義が剥がれました。
この状況で先ほどのように ageFriend コンポーネントに追加してみましょう。

Friend.tsx
 export const FRIEND_FIELDS = gql`
   fragment FriendFields on Friend {
     name
+    age
   }
 `

 export const Friend: React.FC<{ friend: Friend }> = ({ friend }) => {
   return (
     <>
       <p>{friend.name}</p>
+      <p>{friend.age}</p>
    </>
   )
 }

1 ファイルの修正で実装が完了しました。
Friend の関心が Friend コンポーネントにまとまっているため、非常に分かりやすくなったと思います。

今回は非常にシンプルな例を用いましたが、実際のプロダクトではもっとコンポーネントが分割されていることでしょう。
その場合でも「コンポーネントにデータ宣言を記述する」という原則を守って 1 ファイルずつに Fragment を分割して定義することで、Fragment Colocation が実現できます。

さいごに

Fragment Colocation でコンポーネントにデータ宣言を移行することで、コンポーネントの見通しが良くなり保守性があがり、非常に快適になります。

余談ですが Fragment Colocation という名称は日本独特だったりするのでしょうか?
海外の記事やApollo GraphQL Docsでは「colocating fragments」とか「colocated fragments」とか書かれていることが多いような気がするので、英語で検索をする場合はこちらの名称の方がヒットするかもしれません。
(英語圏で主流な名称をご存じの方がいれば是非コメントで教えてください)

参考

GitHubで編集を提案
Gemcook Tech Blog
Gemcook Tech Blog

Discussion