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
の実行はユーザーごとに分けてキャッシュできます。
メトリクスレポート用にアプリケーションのname
とversion
を設定する(有償機能)
[注記] この機能はApollo Studioの有料プランを契約した組織にもっとも有効です。けれど、そうでないアプリケーションすべてにも役立ちます。
ApolloClient
コンストラクタの引数オブジェクトに与えられるオプションが、name
とversion
です。
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に報告する操作トレースにname
とversion
が加えられるのです。クライアントによるセグメントメトリクスが可能になります。