LoginSignup
4
4

More than 1 year has passed since last update.

React + TypeScript: Apollo ClientにおけるGraphQLのベストプラクティス ー 操作の命名やGraphQL変数など

Posted at

Apollo ClientはReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿は公式サイトの「GraphQL query best practices - Operation naming, GraphQL variables, and more」にもとづいて、操作の命名の仕方やGraphQLの変数の使い方など、ベストプラクティスについての解説です。Apollo Clientでクエリを使うための基礎はすでに学んだことが前提となります(まだの方は先に「React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる」をお読みください)。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。

クエリや変更を作成するときは、以下に述べるベストプラクティスにしたがうと、GraphQLとApolloのツールが最大限に活かせるでしょう。

操作にはすべて名前をつける

つぎのコード例のふたつのクエリが取得するデータは同じです。

# 推奨 ✅
query GetBooks {
	books {
		title
	}
}

# 非推奨 ❌
query {
	books {
		title
	}
}

ひとつめのクエリには、GetBooksという名前がつけられています。けれど、ふたつめのクエリには名前がありません

アプリケーションのGraphQL操作には、すべて名前をつけてください。利点はつぎのとおりです。

  • 自分とチームの中で、それぞれの操作の目的が明確になります。
  • ひとつのクエリドキュメントで複数の操作を組み合わせたときに、予期しないエラーが避けられます(操作に名前がないと単体でしか確認できません)。
  • クライアントとサーバーのコードの両方でデバッグ出力が改善します。どの操作が問題を引き起こしているのか正確に絞りやすくなるからです。
  • Apollo Studioの提供する便利な操作レベルのメトリクスを使うには、操作に名前がなければなりません。

引数はGraphQL変数を使って渡す

つぎのコード例のふたつのクエリは、どちらも引数idの値からDogオブジェクトを取得します。

# 推奨 ✅
query GetDog($dogId: ID!) {
	dog(id: $dogId) {
		name
		breed
	}
}

# 非推奨 ❌
query GetDog {
	dog(id: "5") {
		name
		breed
	}
}

ひとつめのクエリは、dogフィールドの必須引数の値に変数($dogId)を渡しました、つまり、このクエリを用いて任意のIDをもつDogオブジェクトが取得できるのです。再利用は大幅にしやすくなります。

useQuery(またはuseMutation)への変数値(id)の渡し方は、つぎの例のとおりです。

const GET_DOG = gql`
	query GetDog($dogId: ID!) {
		dog(id: $dogId) {
			name
			breed
		}
	}
`;
function Dog({ id }) {
	const { loading, error, data } = useQuery(GET_DOG, {
		variables: {
			dogId: id
		},
	});
	// ...コンポーネントのレンダリング...
}

直接記述したGraphQL引数の問題点

引数を直接記述すると、再利用しにくいだけでなく、変数と比べてつぎのように不利な点があります。

キャッシュの効率が下がる

ふたつの同じクエリに、引数だけ異なる値を直接記述した場合、GraphQLサーバーのキャッシュからはまったく異なる操作とみなされるのです。キャッシュが効けば、サーバーは前に行った解析や検証は繰り返しません。その分、パフォーマンスが高まるでしょう。

サーバー側のキャッシュは、フェデレーションゲートウェイでの自動永続クエリやクエリプランなどの機能にも力を発揮します。引数を直接記述してしまっては、こうした機能のパフォーマンスも得られません。キャッシュ内の有用なスペースが占有されてしまいます。

情報プライバシーの低下

GraphQLの値には、アクセストークンやユーザーの個人情報などの機密情報が含まれているかもしれません。この情報がクエリ文字列に含まれていたら、他のクエリ文字列とともにキャッシュされてしまいます。

変数値はクエリ文字列に含まれません。また、Apollo Studioに対して、メトリクスレポートにどの変数値を(もしあれば)含めるかも指定できます。

必要なデータだけを必要な場所で取得する

GraphQLがこれまでのREST APIに比べてきわめて優れている点のひとつは、宣言的データ取得をサポートしていることです。各コンポーネントは、レンダリングに必要なフィールドだけを正確に問い合わせできます(また、そうすべきです)。余計なデータをネットワークに送信することはありません。

それに対して、ルートコンポーネントがひとつの巨大なクエリを実行して、すべての子コンポーネントのデータまで取得したとしましょう。まだ現在の状態でさえレンダリングされていないコンポーネントのクエリも実行してしまいかねません。すると、レスポンスの遅れがもたらされます。クエリの結果を、サーバー側の応答キャッシュで再利用することが大きく妨げられるのです。

ほとんどの場合、つぎの例のようなクエリは複数に分けて、適切なコンポーネントに分散すべきです。

# 非推奨 ❌
query GetGlobalStatus {
	stores {
		id
		name
		address {
			street
			city
		}
		employees {
			id
		}
		manager {
			id
		}
	}
	products {
		id
		name
		price {
			amount
			currency
		}
	}
	employees {
		id
		role
		name {
			firstName
			lastName
		}
		store {
			id
		}
	}
	offers {
		id
		products {
			id
		}
		discount {
			discountType
			amount
		}
	}
}
  • つねに一緒にレンダリングされるコンポーネントのまとまりがあるときは、フラグメントを使いましょう。単一のクエリの構造を複数のコンポーネントの間で分散できます(「フラグメントの配置」参照)。
  • リストフィールドを問い合わせるとき、返される項目数がコンポーネントのレンダリングすべき数より多い場合には、フィールドはページ割りしましょう。

フラグメントを使って関係あるフィールドのまとまりはカプセル化する

GraphQLフラグメントは、複数の操作で共有できるフィールドのまとまりです。

# 推奨 ✅
fragment NameParts on Person {
	title
	firstName
	middleName
	lastName
}

アプリケーションの複数のクエリで、個人のフルネームが必要になったとしましょう。NamePartsフラグメントは、それらのクエリの一貫性を保ち、読みやすく、かつ短くするのに役立つのです。

# 推奨 ✅
query GetAttendees($eventId: ID!) {
	attendees(id: $eventId) {
		id
		rsvp
		...NameParts # NamePartsフラグメントからすべてのフィールドを読み込む
	}
}

過剰であったり論理的でないフラグメントは避ける

フラグメントも、使いすぎるとクエリが読みにくくなります。

# 要注意 ⚠️
query GetAttendees($eventId: ID!) {
	attendees(id: $eventId) {
		id
		rsvp
		...NameParts
		profile {
			...VisibilitySettings
			events {
				...EventSummary
			}
			avatar {
				...ImageDetails
			}
		}
	}
}

さらに、論理的な意味関係を共有するフィールドのまとまりのみフラグメントとして定めてください。いくつかのクエリがたまたま特定のフィールドを共有するからといって、フラグメントにすべきではありません。

# 推奨 ✅
fragment NameParts on Person {
	title
	firstName
	middleName
	lastName
}
# 非推奨 ❌
fragment SharedFields on Country {
	population
	neighboringCountries {
		capital
		rivers {
			name
		}
	}
}

グローバルなデータとユーザー固有のデータは別々に問い合わせる

フィールドによっては、どのユーザーから問い合わせても返されるデータはまったく同じかもしれません。

# 周期表のすべての要素を返す
query GetAllElements {
	elements {
		atomicNumber
		name
		symbol
	}
}

けれど、クエリを実行するユーザーに応じて異なるデータが返されるフィールドもあります。

# 現在のユーザーのドキュメントを返す
query GetMyDocuments {
	myDocuments {
		id
		title
		url
		updatedAt
	}
}

サーバー側のレスポンスキャッシュのパフォーマンスを高めるためには、フィールドがグローバルかユーザー固有かによってできるかぎり分けて取得してください。そうすることで、前掲GetAllElementsのようなクエリは単一のレスポンスをキャッシュし、GetMyDocumentsの実行はユーザーごとに分けてキャッシュできます。

メトリクスレポート用にアプリケーションのnameversionを設定する(有償機能)

[注記] この機能はApollo Studioの有料プランを契約した組織にもっとも有効です。けれど、そうでないアプリケーションすべてにも役立ちます。

ApolloClientコンストラクタの引数オブジェクトに与えられるオプションが、nameversionです。

const client = new ApolloClient({
	uri: 'http://localhost:4000/graphql',
	cache: new InMemoryCache(),
	name: 'MarketingSite',
	version: '1.2'
});

これらの値を定めると、Apollo Clientは自動的にHTTPヘッダーとして加えます。

  • apolllographql-client-name
  • apolllographql-client-version

そして、Apollo Studioでメトリクスレポートを設定すれば、Apollo ServerからStudioに報告する操作トレースにnameversionが加えられるのです。クライアントによるセグメントメトリクスが可能になります。

4
4
0

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
  3. You can use dark theme
What you can do with signing up
4
4