Apollo ClientはReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿は公式サイトの「Fragments Share fields between operations」にもとづいて、複数のクエリや変更の操作間でフィールドをどう共有するかについての解説です。Apollo Clientでクエリを使うための基礎はすでに学んだことが前提となります(まだの方は先に「React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる」をお読みください)。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。
GraphQLのフラグメントは、複数のクエリと変更の間で共有できるロジックの一部です。つぎのコード例は、NameParts
フラグメントの宣言で、任意のPerson
オブジェクトで使えます。
fragment NameParts on Person {
firstName
lastName
}
フラグメントには、関連する型に属するフィールドのサブセットが含まれます。上記の例では、NameParts
フラグメントが有効であるためには、Person
型にフィールドとしてfirstName
とlastName
が宣言されていなければなりません。
これで、Person
オブジェクトを参照する多くのクエリや変更に、NameParts
フラグメントが加えられます。
query GetPerson {
people(id: "7") {
...NameParts
avatar(size: LARGE)
}
}
[注記] フラグメントの前に添えた...
は、JavaScriptのスプレッド構文と同じ役割です。
前掲NameParts
の定めにもとづくと、上記クエリはつぎのコードと等しくなります。
query GetPerson {
people(id: "7") {
firstName
lastName
avatar(size: LARGE)
}
}
フラグメントを用いたフィールドの更新は自動です。NameParts
フラグメントのフィールドがあとから書き替えられると、フラグメントを使った操作のフィールドに反映されます。複数の操作にまたがるフィールドの一貫性を保ち、手間が省けるのです。
使い方の例
たとえば、ブログアプリケーションがあって、コメントに関わるいくつかのGraphQL操作(コメントの投稿、記事のコメントの取得など)を実行するとしましょう。これらの操作のおそらくすべてに、Comment
型のフィールドが含まれる想定です。
このコアとなるフィールドのセットを指定するために、Comment
型につぎのようなフラグメント(CoreCommentFields
)が定められます。
import { gql } from '@apollo/client';
export const CORE_COMMENT_FIELDS = gql`
fragment CoreCommentFields on Comment {
id
postedBy {
username
displayName
}
createdAt
content
}
`;
[注記] フラグメントは、アプリケーションの中であれば、どのファイルで宣言しても構いません。上記の例は、ファイルfragments.js
からフラグメントをexport
しました。
すると、同じアプリケーションのファイルPostDetails.jsx
は、CORE_COMMENT_FIELDS
をimport
して、GraphQLの操作にCoreCommentFields
フラグメントが加えられるのです。
import { gql } from '@apollo/client';
import { CORE_COMMENT_FIELDS } from './fragments';
export const GET_POST_DETAILS = gql`
${CORE_COMMENT_FIELDS}
query CommentsForPost($postId: ID!) {
post(postId: $postId) {
title
body
author
comments {
...CoreCommentFields
}
}
}
`;
// ...PostDetailsコンポーネントの定義...
- 外部ファイル
fragments.js
で宣言され、export
されたCORE_COMMENT_FIELDS
をimport
する。 -
GET_POST_DETAILS gql
テンプレートリテラルに、プレースホルダー(${CORE_COMMENT_FIELDS}
)でフラグメントの定義を加える。 -
CoreCommentFields
フラグメントを、標準の...
構文でクエリに含める。
フラグメントの配置
GraphQLレスポンスのツリー状の構造は、フロントエンドでレンダリングされるコンポーネントの階層に似ています。この類似性があるため、フラグメントを用いることにより、クエリロジックがコンポーネント間で切り分けできるのです。各コンポーネントは、それぞれが使うフィールドを正確に要求できるようになります。コンポーネントロジックもより簡潔になるでしょう。
つぎのようなビュー階層のアプリケーションを考えてみます。
FeedPage
└── Feed
└── FeedEntry
├── EntryInfo
└── VoteButtons
このアプリケーションでクエリを実行するのは、ルートのFeedPage
コンポーネントです。FeedEntry
オブジェクトのリストを取得します。EntryInfo
とVoteButtons
の子コンポーネントは、上位のFeedEntry
オブジェクトから必要なフィールドを得なければなりません。
一緒に配置されたフラグメントの作成
一緒に配置された(colocated)フラグメントというのは、普通のフラグメントと大きく変わりません。違いは、フラグメントのフィールドを使う特定のコンポーネントに加えられることです。たとえば、FeedPage
の子コンポーネントVoteButtons
が、フィールドscore
とvote { choice }
をFeedEntry
オブジェクトから受け取れます。
VoteButtons.fragments = {
entry: gql`
fragment VoteButtonsFragment on FeedEntry {
score
vote {
choice
}
}
`,
};
子コンポーネントVoteButtons.jsx
でフラグメントを定めたあと、親のFeedEntry.jsx
はそれをつぎのように一緒に配置された自分のフラグメントの中で参照できるのです(EntryInfo
のフラグメントのコード例は省略されています)。
FeedEntry.fragments = {
entry: gql`
fragment FeedEntryFragment on FeedEntry {
commentCount
repository {
full_name
html_url
owner {
avatar_url
}
}
...VoteButtonsFragment
...EntryInfoFragment
}
${VoteButtons.fragments.entry}
${EntryInfo.fragments.entry}
`,
};
VoteButtons.fragments.entry
やEntryInfo.fragments.entry
といった名前のつけ方は、とくに決まっていません。コンポーネントを指定してそのフラグメントが得られさえすれば、どのような命名規則でも使えるのです。
Webpack使用時のフラグメントのインポート
.graphql
ファイルをgraphql-tag/loader
で読み込む場合、つぎのようにimport
ステートメントによりフラグメントが含められます。
#import "./someFragment.graphql"
これで、someFragment.graphql
の内容が、現在のファイルから使えるようになるのです。詳しくは、「Loading queries with Webpack」の「Fragments」の項をご参照ください。
フラグメントをユニオン型とインタフェースで使う
フラグメントはユニオン型やインタフェースにも定められます。つぎの例は、3つのインラインフラグメントを含むクエリです。
query AllCharacters {
all_characters {
... on Character {
name
}
... on Jedi {
side
}
... on Droid {
model
}
}
}
上記コード例のクエリall_characters
は、Character
オブジェクトのリストを返します。Character
型は、Jedi
とDroid
がともに実装するインタフェースです。リストに含まれる各項目には、Jedi
型のオブジェクトならside
フィールド、Droid
型ではmodel
フィールドが加わります。
けれど、このクエリが機能するためには、クライアントはインタフェースCharacter
と実装する型との間の多態な関係をわかっていなければなりません。そうした関係をクライアントに知らせるため、InMemoryCache
の初期化時にpossibleTypes
オプションが渡せます。
possibleTypes
を手動で定める
[注記] possibleTypes
オプションはApollo Client 3.0以降から使えます。
possibleTypes
は、InMemoryCache
コンストラクタに渡すオプションで、スキーマへのスーパータイプ/サブタイプの関係の指定です。このオブジェクトが、インタフェースまたはユニオン型の名前(スーパータイプ)を、実装または属する型(サプタイプ)へとマップします。
つぎのコードが、possibleTypes
を宣言する例です。
const cache = new InMemoryCache({
possibleTypes: {
Character: ["Jedi", "Droid"],
Test: ["PassingTest", "FailingTest", "SkippedTest"],
Snake: ["Viper", "Python"],
},
});
このコード例では、3つのインタフェース(Character
とTest
、およびSnake
)とそれらを実装するオブジェクト型が示されています。
スキーマに含まれるユニオン型やインタフェースが少なければ、おそらく手動でpossibleTypes
を指定しても問題ありません。けれど、スキーマのサイズや複雑さが増してくると、つぎに解説するような、スキーマから「possibleTypes
を自動的に生成する」方法を検討するべきでしょう。
possibleTypes
を自動的に生成する
つぎのコード例は、GraphQLイントロスペクションクエリをpossibleTypes
設定オブジェクトに変換します。
const fetch = require('cross-fetch');
const fs = require('fs');
fetch(`${YOUR_API_HOST}/graphql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
variables: {},
query: `
{
__schema {
types {
kind
name
possibleTypes {
name
}
}
}
}
`,
}),
}).then(result => result.json())
.then(result => {
const possibleTypes = {};
result.data.__schema.types.forEach(supertype => {
if (supertype.possibleTypes) {
possibleTypes[supertype.name] =
supertype.possibleTypes.map(subtype => subtype.name);
}
});
fs.writeFile('./possibleTypes.json', JSON.stringify(possibleTypes), err => {
if (err) {
console.error('Error writing possibleTypes.json', err);
} else {
console.log('Fragment types successfully extracted!');
}
});
});
こうすれば、生成されたpossibleTypes
JSONモジュールが、InMemoryCache
をつくるファイルにimport
できるのです。
import possibleTypes from './path/to/possibleTypes.json';
const cache = new InMemoryCache({
possibleTypes,
});