ReactでFragment Colocationする時にFragmentデータの型をどうするか
GraphQL / TypeScript / 型
ReactでFragment Colocation
Fragment Colocationというのは「GraphQLスキーマを、コンポーネントと同じファイルで定義しましょう」という考え方です。
React + Apollo Client + GraphQL Code Generator(v3) の場合、例えばこんな感じになります。
// UserPage.tsx
const FIND_USER_QUERY = gql`
  query FindUser {
    user {
      id
      name
      books {
        ...BookFieldsForBookList
      }
    }
  }
`
export const UserPage: FC = () => {
  const { data } = useQuery(FIND_USER_QUERY)
  if (!data) return null
  const { user } = data
  return (
    <>
      <div>{user.name}の本</div>
      <BookList bookFragments={user.books} />
    </>
  )
}
// BookList.tsx
const BOOK_FIELDS_FRAGMENT = gql`
  fragment BookFieldsForBookList on Book {
    id
    name
    author {
      id
      name
    }
  }
`
const MaskedBookFragment = FragmentType<typeof BOOK_FIELDS_FRAGMENT>
const useBookFragment = (
  bookFragment: MaskedBookFragment,
): BookFieldsForBookListFragment => useFragment(BOOK_FIELDS_FRAGMENT, book),
export const BookList: FC<{ bookFragments: MaskedBookFragment[] }> = ({ bookFragments }) => {
  const books = bookFragments.map(useBookFragment)
  return (
    <ul>
      {books.map((book) => (
        <li key={book.id}>
          {book.name}({book.author.name})
        </li>
      ))}
    </ul>
  )
}
このように、BookListで必要なfieldをBookList.tsxのGraphQL Fragmentで定義することで、BookListで扱うfieldをBookList.tsx内で管理できるようになります。
graphql-codegenが生成するFragmentの型
graphql-codegenを使うとGraphQLのQueryやFragmentの戻り値の型を自動生成してくれます。
今回の例だと以下のようにFindUserQueryとBookFieldsForBookListFragmentという型定義が作られます。
export type FindUserQuery = {
  __typename?: 'Query'
  user: {
    __typename?: 'User'
    name: string
    books: Array<
      { __typename?: 'Book' } & {
        ' $fragmentRefs'?: {
          BookFieldsForBookListFragment: BookFieldsForBookListFragment
        }
      }
    >
  }
}
export type BookFieldsForBookListFragment = {
  __typename?: 'Book'
  id: string
  name: string
  author: { __typename?: 'Author'; name: string }
} & { ' $fragmentName'?: 'BookFieldsForBookListFragment' }
GraphQL Code GeneratorにはFragment Maskingという仕組みがあり、見ての通りFindUserQueryのuser.booksからは直接Bookデータは取得できません。
Bookの中身を取得するには型変換する必要があり、その処理をしているのがこちらのuseBookFragment関数になります。
const useBookFragment = (
  bookFragment: MaskedBookFragment,
): BookFieldsForBookListFragment => useFragment(BOOK_FIELDS_FRAGMENT, book)
useFragmentはgraphql-codegenが自動生成する関数で、Fragmentの型変換をしてくれます。
型変換することでBookのidやnameなどが取得できるようになります。
Apollo ClientのuseFragment
Apollo Clientにも同じ名前のuseFragmentというフック関数があるのですが、こちらはgraphql-codegenとは異なりFragmentキャッシュからデータを取得するための関数になっています。
(@teppeitaさん、ありがとうございます!)
graphql-codegenの設定でuseFragmentの関数名を変更することもできるので、Apollo ClientのuseFragmentと併用する場合などは、関数名を変更しておいた方が良さそうです。
Fragment Maskingをしたくない場合
Fragment MaskingをするとFragment Colocationで定義したFragmentを他のコンポーネントに使わせないということがやりやすくなるのですが、逆に他のコンポーネントでそのデータを使いたいというケースもあるかと思います。
そのような場合はFragment Maskingを無効化することもできます。
このあたりはどういう方針でFragment Colocationをするかによって決めるのが良さそうです。
結局Fragment Maskingは導入すべき?
実際に導入してみての感想としては、他のコンポーネントへの影響を心配せずにFragmentのfieldを変更できるというはメリットは大きいと感じています。
ですので、Fragment ColocationをするのであればFragment Maskingは有効にすることをオススメします。
まとめ
React + Apollo + graphql-codegenでFragment Collocationをする場合はuseFragmentを使いましょう。
