React + TypeScript: Apollo Clientのフラグメントで操作間のフィールドを共有する

[Apollo Client](https://www.apollographql.com/docs/react/)はReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿は公式サイトの「[Fragments Share fields between operations](https://www.apollographql.com/docs/react/data/fragments/)」にもとづいて、複数のクエリや変更の操作間でフィールドをどう共有するかについての解説です。Apollo Clientでクエリを使うための基礎はすでに学んだことが前提となります(まだの方は先に「[React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる](https://qiita.com/FumioNonaka/items/0c6b711627e3443ff73b)」をお読みください)。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。

[GraphQLのフラグメント](http://graphql.org/learn/queries/#fragments)は、複数のクエリと変更の間で共有できるロジックの一部です。つぎのコード例は、`NameParts`フラグメントの宣言で、任意の`Person`オブジェクトで使えます。

```graphql
fragment NameParts on Person {
	firstName
	lastName
}
```

フラグメントには、関連する型に属するフィールドのサブセットが含まれます。上記の例では、`NameParts`フラグメントが有効であるためには、`Person`型にフィールドとして`firstName`と`lastName`が宣言されていなければなりません。

これで、`Person`オブジェクトを参照する多くのクエリや変更に、`NameParts`フラグメントが加えられます。

```graphql
query GetPerson {
	people(id: "7") {
		...NameParts
		avatar(size: LARGE)
	}
}
```

[注記] フラグメントの前に添えた`...`は、JavaScriptの[スプレッド構文](https://qiita.com/FumioNonaka/items/58358a29850afd7a0f37#%E3%82%B9%E3%83%97%E3%83%AC%E3%83%83%E3%83%89%E6%A7%8B%E6%96%87%E3%82%92%E4%BD%BF%E3%81%86)と同じ役割です。

前掲`NameParts`の定めにもとづくと、上記クエリはつぎのコードと等しくなります。

```graphql
query GetPerson {
	people(id: "7") {
		firstName
		lastName
		avatar(size: LARGE)
	}
}
```

フラグメントを用いたフィールドの更新は自動です。`NameParts`フラグメントのフィールドがあとから書き替えられると、フラグメントを使った操作のフィールドに反映されます。複数の操作にまたがるフィールドの一貫性を保ち、手間が省けるのです。


# 使い方の例
たとえば、ブログアプリケーションがあって、コメントに関わるいくつかのGraphQL操作(コメントの投稿、記事のコメントの取得など)を実行するとしましょう。これらの操作のおそらくすべてに、`Comment`型のフィールドが含まれる想定です。

このコアとなるフィールドのセットを指定するために、`Comment`型につぎのようなフラグメント(`CoreCommentFields`)が定められます。

```javascript: fragments.js
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`フラグメントが加えられるのです。

```javascript: PostDetails.jsx
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コンポーネントの定義...
```

1. 外部ファイル`fragments.js`で宣言され、`export`された`CORE_COMMENT_FIELDS`を`import`する。
2. `GET_POST_DETAILS gql`テンプレートリテラルに、プレースホルダー（`${CORE_COMMENT_FIELDS}`）でフラグメントの定義を加える。
3. `CoreCommentFields`フラグメントを、標準の`...`構文でクエリに含める。


# フラグメントの配置
GraphQLレスポンスのツリー状の構造は、フロントエンドでレンダリングされるコンポーネントの階層に似ています。この類似性があるため、フラグメントを用いることにより、クエリロジックがコンポーネント間で切り分けできるのです。各コンポーネントは、それぞれが使うフィールドを正確に要求できるようになります。コンポーネントロジックもより簡潔になるでしょう。

つぎのようなビュー階層のアプリケーションを考えてみます。

```
FeedPage
└── Feed
    └── FeedEntry
        ├── EntryInfo
        └── VoteButtons
```

このアプリケーションでクエリを実行するのは、ルートの`FeedPage`コンポーネントです。`FeedEntry`オブジェクトのリストを取得します。`EntryInfo`と`VoteButtons`の子コンポーネントは、上位の`FeedEntry`オブジェクトから必要なフィールドを得なければなりません。


## 一緒に配置されたフラグメントの作成
一緒に配置された(colocated)フラグメントというのは、普通のフラグメントと大きく変わりません。違いは、フラグメントのフィールドを使う特定のコンポーネントに加えられることです。たとえば、`FeedPage`の子コンポーネント`VoteButtons`が、フィールド`score`と`vote { choice }`を`FeedEntry`オブジェクトから受け取れます。

```javascript:VoteButtons.jsx
VoteButtons.fragments = {
	entry: gql`
		fragment VoteButtonsFragment on FeedEntry {
			score
			vote {
				choice
			}
		}
	`,
};
```

子コンポーネント`VoteButtons.jsx`でフラグメントを定めたあと、親の`FeedEntry.jsx`はそれをつぎのように一緒に配置された自分のフラグメントの中で参照できるのです(`EntryInfo`のフラグメントのコード例は省略されています)。

```javascript:FeedEntry.jsx
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`](https://github.com/apollographql/graphql-tag/blob/main/loader.js)で読み込む場合、つぎのように`import`ステートメントによりフラグメントが含められます。

```graphql
#import "./someFragment.graphql"
```

これで、`someFragment.graphql`の内容が、現在のファイルから使えるようになるのです。詳しくは、「Loading queries with Webpack」の「[Fragments](https://www.apollographql.com/docs/react/integrations/webpack/#fragments)」の項をご参照ください。


# フラグメントをユニオン型とインタフェースで使う
フラグメントは[ユニオン型やインタフェース](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/)にも定められます。つぎの例は、3つの[インラインフラグメント](https://qiita.com/FumioNonaka/items/f22bfc2928d56469bf93#%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E3%83%95%E3%83%A9%E3%82%B0%E3%83%A1%E3%83%B3%E3%83%88)を含むクエリです。

```graphql
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`を宣言する例です。

```javascript
const cache = new InMemoryCache({
	possibleTypes: {
		Character: ["Jedi", "Droid"],
		Test: ["PassingTest", "FailingTest", "SkippedTest"],
		Snake: ["Viper", "Python"],
	},
});
```

このコード例では、3つのインタフェース(`Character`と`Test`、および`Snake`)とそれらを実装するオブジェクト型が示されています。

スキーマに含まれるユニオン型やインタフェースが少なければ、おそらく手動で`possibleTypes`を指定しても問題ありません。けれど、スキーマのサイズや複雑さが増してくると、つぎに解説するような、スキーマから「`possibleTypes`を自動的に生成する」方法を検討するべきでしょう。


## `possibleTypes`を自動的に生成する
つぎのコード例は、GraphQLイントロスペクションクエリを`possibleTypes`設定オブジェクトに変換します。

```javascript
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`できるのです。

```javascript
import possibleTypes from './path/to/possibleTypes.json';

const cache = new InMemoryCache({
	possibleTypes,
});
```