search
LoginSignup
0
Help us understand the problem. What are the problem?

posted at

updated at

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

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型にフィールドとしてfirstNamelastNameが宣言されていなければなりません。

これで、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)が定められます。

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_FIELDSimportして、GraphQLの操作にCoreCommentFieldsフラグメントが加えられるのです。

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_FIELDSimportする。
  2. GET_POST_DETAILS gqlテンプレートリテラルに、プレースホルダー(${CORE_COMMENT_FIELDS})でフラグメントの定義を加える。
  3. CoreCommentFieldsフラグメントを、標準の...構文でクエリに含める。

フラグメントの配置

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

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

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

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

一緒に配置されたフラグメントの作成

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

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

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

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.entryEntryInfo.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型は、JediDroidがともに実装するインタフェースです。リストに含まれる各項目には、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つのインタフェース(CharacterTest、および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!');
			}
		});
	});

こうすれば、生成されたpossibleTypesJSONモジュールが、InMemoryCacheをつくるファイルにimportできるのです。

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0
Help us understand the problem. What are the problem?