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という仕組みがあり、見ての通りFindUserQueryuser.booksからは直接Bookデータは取得できません。

Bookの中身を取得するには型変換する必要があり、その処理をしているのがこちらのuseBookFragment関数になります。

const useBookFragment = (
  bookFragment: MaskedBookFragment,
): BookFieldsForBookListFragment => useFragment(BOOK_FIELDS_FRAGMENT, book)

useFragmentはgraphql-codegenが自動生成する関数で、Fragmentの型変換をしてくれます。

型変換することでBookidnameなどが取得できるようになります。

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を使いましょう。