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