始めに
Reactを使ったGraphQLのFragment Colocation
に関する記事です。
GraphQL APIを利用するフロントエンド開発では、Fragment Colocationは非常に便利です。しかしながら、初めてGraphQLを利用したときは、Fragmentの重要性に気づきませんでした。そこで参考になればと思い、以前身内で発表したものを少し肉付けして、記事にしました。(なお私もそんなにわかっているわけではないので悪しからず)
主に以下ついて述べます。
- GraphQLのFragmentとは
- Fragment Colocationとは
- Fragment Colocationは何を変えるか
- Typescriptとの併用について
Reactの利用を前提として記事を書いていますが、コンポーネント指向なフロントエンドであれば、主旨は変わらないと思います。
Fragment is 何
Fragmentは、GraphQLを構成する単位の一つであり、重複するfieldsをまとめる機能があります。以下はspec.graphqlにのっている例です。
まずはFragmentを使わない例です。あるuserのfriends
とmutualFriends
を取得しています。しかし、queryを見るとわかりますが、friends
とmutualFriends
の中のfieldは全く同じです。
query noFragments {
user(id: 1) {
friends {
id
name
profilePic
}
mutualFriends {
id
name
profilePic
}
}
}
次に以下がFragmentを利用して重複を排除した例です。
query withFragments {
user(id: 4) {
friends {
...friendFields
}
mutualFriends {
...friendFields
}
}
}
fragment friendFields on User {
id
name
profilePic
}
これは最初の例と全く同じqueryを表現していますが、friendFields
Fragmentを利用して重複していたfieldの共通化が可能になりました。
Fragmentはスプレッド演算子によって使われることに注意しましょう。また、on User
は既に定義してあるオブジェクトの型です。Fragmentはon句でオブジェクトを指定して、そのオブジェクトが持つfieldを定義することができます。なので、User
オブジェクトは以下のように定義されているはずです。
type User {
id
name
profilePic
... # その他のfield
}
上記のように、Fragmentを用いると重複するfieldを共通化してDRYなqueryを書くことができました。
これだけだと、ただのFieldの共通化でそれほど魅力的な機能に見えないかもしれません。しかし、Reactのコンポーネントと組み合わせることでより強力に使いこなすことができます。
Fragment Colocation
Fragment ColocationはApolloのDocsによると
A colocated fragment is just like any other fragment, except it is attached to a particular component that uses the fragment's fields.
意訳すると
「colocated fragmentとは、特定のコンポーネントで利用するFragmentをそのコンポーネントに付け加えることだ」
とあります。
といってもわかりにくいので例を上げて説明します。
例えば、先程のfriendを用いたページを考えてみましょう。以下にFragmentを利用していないqueryを用意します。
query noFragments {
friend {
id
name
profilePic
}
}
一方、queryを利用する側のコンポーネントを擬似的に以下のように書きます。
const Index = () => {
const friend = execute(query) //queryを利用してAPIから取得
return (
<>
<Hoge />
<Fuga />
<Friend friend={friend} />
</>
)
}
export const Friend = (friend) => {
return (
<>
<p>friend.id</p>
<img>friend.profilePic</img>
<p>friend.name</p>
</>
)
}
上記をFragment Colocationの手法で書き換えます。
やり方は、Friend
コンポーネントで利用するFragmentを用意し、そのFragmentを利用するFriend
コンポーネントに配置するだけです。
まずFragmentを利用するようにquery書き換えます。
query withFragments {
friend {
...friendFiedls
}
}
続いて、そのFragmentをFriendコンポーネントと同じ場所に書きます。
export const Friend_Feilds = gql`
fragment friendFields on User {
id
name
profilePic
}
`
export const Friend = (friend) => {
return (
<>
<p>friend.id</p>
<img>friend.profilePic</img>
<p>friend.name</p>
</>
)
}
これで終わりです。あっけないですね。一体何が変わったのでしょうか。
Fragment Colocationで何が変わったか
まずnoFragments
queryを利用した場合を考えます。このとき noFragments
queryはFriend
コンポーネントで使われるプロパティを知っている必要があります。そのため、Friend
コンポーネントで使うプロパティに性別(gender)が追加された場合、queryの定義にgenderを追加する必要があります。
一方、withFragments
を使った場合はどうでしょう。性別のプロパティの追加によって変更するのはfriendFields
Fragmentです。
大元のwithFragments
queryはFriend
コンポーネントで必要なプロパティを知っている必要がありません。
queryが知っているのは、friendFields
FragmentがFriend
コンポーネントに配置(colocated)されるFragmentであることだけです。
Friend
コンポーネントで使われるプロパティ(具象)に依存していたのが、friendFeilds
という抽象に依存するようになったのです。
次にFragmentをFriend
コンポーネントと同じ場所に配置したことで、取得するデータを利用するコンポーネントと同じ場所で定義することができました。取得するデータを決めるのはそのデータを使うコンポーネントであるという責務を明確にし、凝集度(?)が高くなり見通しがよくなります。
実はコンポーネントごとのFragmentは使うが、コンポーネントと同じファイルに置くのは嫌だという流派の人もいます。(私です)
ここまででFragment Colocationの説明は終わりです。ここからは、FragmentはTypescriptとも相性が良いという話をします。
Typescriptと型生成
GraphQLを利用する多くのプロジェクトはTypescriptを利用しており、GraphQLのスキーマから自動で型を生成していると思います。
ここで面倒なことがあります。GraphQLのqueryの戻り値型(これは自動生成)からあるコンポーネントの引数となる型のみを作ることです。
例えば大元のページにFrined
コンポーネントとArticle
コンポーネントがあるとしましょう。この2つのコンポーネントで利用するデータは以下のように1つのQueryで取得することができます。
query user {
user(id: 1) {
friends {
id
name
}
articles {
id
title
}
}
}
それぞれのコンポーネントでは違うデータが必要なので、それぞれ引数の型を作る必要があります。自動生成されたqueryの戻り値型からFriend用とArticle用の型を抜き出すのは意外と面倒です。(この程度なら難しくはないですが、実際はもっと複雑です)
いちいち自分で作るのも面倒だという人のために多くの型生成ライブラリはFragmentの型も自動で作ってくれます。
それぞれのコンポーネントで使うデータをFragmentとして定義しておけば、引数の型として必要なものも自動で出来上がります。
これは非常に便利です。
以上、Fragment Colocationの説明でした。最近フロント触ってないのでもっと便利なものがあるかもしれません。