Apollo ClientはReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿は公式サイトの「Refetching queries in Apollo Client」にもとづいて、Apollo ClientのメソッドでクライアントサイドのGraphQLデータをどう更新するかについての解説です。Apollo Clientでクエリを使うための基礎はすでに学んだことが前提となります(まだの方は先に「React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる」をお読みください)。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。
Apollo Clientは、キャッシュの更新によりGraphQLデータをローカルで変更できます(「update関数で項目データをリストに書き加える」参照)。けれど、クライアント側のGraphQLデータは、サーバーからクエリを再取得(refetch)して更新した方が端的な場合もあるでしょう。
理屈では、クライアントサイドが更新されたら、すべてのアクティブなクエリを再取得することもできます。けれど、リフェッチするクエリを選べば、時間とネットワーク帯域幅が節約できるはずです。InMemoryCacheは、最近のキャッシュの更新によって有効性が失われたかもしれないアクティブなクエリを特定するのに役立ちます。
ローカルキャッシュの更新をリフェッチと組み合わせると、特に効果的です。アプリケーションは、ローカルキャッシュの変更結果を直ちに表示します。同時に、バックグラウンドでリフェッチし、サーバーから最新のデータを取得することができるからです。すると、ローカルデータとリフェッチされたデータの間に差異がある場合にのみ、UIが再レンダリングされます。
リフェッチをよく行うのは変更のあとです。そのため、変更関数はrefetchQueriesやonQueryUpdatedなどのオプションを受け取り、どのクエリをどのようにリフェッチするか指定できます(変更関数については「useMutationフックを使う」参照)。
変更の外でクエリを選んでリフェッチしたいときは、ApolloClientのrefetchQueriesメソッドをお使いください。構文の解説はつぎのとおりです。
client.refetchQueries
リフェッチオプション
client.refetchQueriesメソッドは、つぎのTypeScriptインタフェースに準拠したオプションオブジェクトを受け取ります。
interface RefetchQueriesOptions<
TCache extends ApolloCache<any>,
TResult = Promise<ApolloQueryResult<any>>,
> {
updateCache?: (cache: TCache) => void;
include?: Array<string | DocumentNode> | "all" | "active";
onQueryUpdated?: (
observableQuery: ObservableQuery<any>,
diff: Cache.DiffResult<any>,
lastDiff: Cache.DiffResult<any> | undefined,
) => boolean | TResult;
optimistic?: boolean;
}
| 名前/型 | 説明 |
|---|---|
updateCache: (cache: TCache) => void |
キャッシュされたフィールドを更新し、そのフィールドが含まれるクエリのリフェッチを開始する関数(省略可)。 |
include: Array<string | DocumentNode> | "all" | "active" |
リフェッチするクエリを指定するオプションの配列(省略可)。要素はクエリの文字列か、DocumentNodeオブジェクトのいずれか(変更のoptions.refetchQueries配列と同じ)。すべての(アクティブな)クエリをリフェッチするための省略記法が"active"(または "all")。 |
onQueryUpdated:(observableQuery: ObservableQuery<any>, diff: Cache.DiffResult<any>, lastDiff: Cache.DiffResult<any> | undefined) => boolean | TResult |
各ObservableQueryに対して1度ずつ呼び出されるコールバック関数(省略可)。ObservableQueryは、options.updateCacheの影響を受けるか、options.includeにリストされたもの(あるいはその両方)。onQueryUpdatedが与えられないと、デフォルトの実装はobservableQuery.refetch()の呼び出し結果を返す。onQueryUpdatedが渡されると、どのクエリをどうリフェッチすべきか動的に決められる。onQueryUpdatedからfalseを返すと、関連するクエリがリフェッチされない。 |
optimistic: boolean |
trueが渡されると、options.updateCacheはInMemoryCacheの一時的な楽観レイヤーで実行される。すると、どのフィールドを無効にしたかを確認してから、キャッシュからその変更が破棄できる。デフォルト値はfalseで、options.updateCacheはキャッシュを永続的に更新する。 |
リフェッチ結果
client.refetchQueriesメソッドは、onQueryUpdatedが返すTResultの結果を収集します。デフォルトはTResult = Promise<ApolloQueryResult<any>>です。onQueryUpdatedが与えられていない場合、Promise.all(results)を用いて結果をひとつのPromise<TResolved[]>にまとめます。
[注記] Promise.allのPromiseアンラップ動作のおかげで、TResolvedは大抵TResultと同じ型です。ただし、TResultがPromiseLike<TResolved>またはbooleanの場合を除きます。
返されたPromiseオブジェクトはほかにふたつの便利なプロパティを備えます。
| 名前/型 | 説明 |
|---|---|
queries: ObservableQuery[] |
リフェッチされたObservableQueryオブジェクトの配列。 |
results: TResult[] |
結果の配列。onQueryUpdatedが渡されたときは、その戻り値が要素となる。与えられない場合は、デフォルトで返される結果(待機(pending)のPromiseを含む)。あるクエリに対してonQueryUpdatedがfalseを返すと、そのクエリに対する結果は提供されない。onQueryUpdatedがtrueを返した場合、結果の Promise<ApolloQueryResult<any>>はtrueではなく、結果の配列に含まれる。 |
ふたつの配列は、互いに対応します。配列の長さは同じです。任意のインデックスiについて、queries[i]のObservableQueryにonQueryUpdatedが呼ばれたとき、生成された結果がresults[i]となります。
リフェッチレシピ
特定のクエリをリフェッチする
再取得したいのが特定のクエリのときは、includeオプションを単独で用いて名前を与えます。
await client.refetchQueries({
include: ["SomeQueryName"],
});
includeオプションに渡すのはDocumentNodeでも、特定のクエリを再取得することが可能です。
await client.refetchQueries({
include: [SOME_QUERY],
});
すべてのクエリをリフェッチする
アクティブなクエリをすべて再取得するには、includeに省略表記の"active"を渡してください。
await client.refetchQueries({
include: "active",
});
アクティブでないクエリまですべて含めてフェッチしたいとき、include渡す省略表記は"all"です。オブザーバーをもたなかったり、コンポーネントがマウントされていないクエリまで含まれます。
await client.refetchQueries({
include: "all", // "active"を使うべきか判断する
});
キャッシュ更新の影響を受けたクエリのリフェッチ
updateCacheコールバックで実行されたキャッシュ更新の影響が及んだクエリを再取得することができます。
await client.refetchQueries({
updateCache(cache) {
cache.evict({ fieldName: "someRootField" });
},
});
リフェッチされるのは、Query.someRootFieldに依存するすべてのクエリです。どのクエリが含まれるか、あらかじめ知らなくて構いません。updateCacheには、任意のキャッシュ操作(writeQuery、writeFragment、modify、evictなど)が組み合わせられるのです。
updateCacheが実行した更新は、デフォルトでキャッシュに残ります。一時的な楽観レイヤーで実行すれば、client.refetchQueriesが更新を監視し終えたあと、キャッシュを変更せずにすぐに破棄することも可能です。
await client.refetchQueries({
updateCache(cache) {
cache.evict({ fieldName: "someRootField" });
},
// 楽観レイヤーで、Query.someRootFieldを一時的に削除
optimistic: true,
});
キャッシュデータを実際に変更することなく、キャッシュを「更新」するには、もうひとつやり方があります。cache.modifyとそのINVALIDATEセンチネルオブジェクトを用いることです。
await client.refetchQueries({
updateCache(cache) {
cache.modify({
fields: {
someRootField(value, { INVALIDATE }) {
// Query.someRootFieldが含まれるクエリを
// キャッシュ内の値は実際に変更することなく更新
return INVALIDATE;
},
},
});
},
});
[注記] client.refetchQueriesが導入される前は、INVALIDATEセンチネルはあまり役に立ちませんでした。fetchPolicy: "cache-first"での無効なクエリは通常、変更されていない結果を再読み取りするため、ネットワークの要求は実行しないことにしてしまうからです。client.refetchQueriesメソッドは、この無効化システムをアプリケーションコードにアクセスしやすくするので、無効になったクエリのリフェッチ動作が制御できるようになります。
これまで掲げたコード例は、includeとupdateCacheのどちらを使っても、ネットワークから影響されるクエリをリフェッチします。そして、結果のPromise<ApolloQueryResult<any>をclient.refetchQueriesが返すPromise<TResolved>に含めるのです。
特定のクエリがincludeとupdateCacheの両方に含まれても、そのクエリは一度だけ再取得されます。つまり、includeオプションは、updateCacheにどのクエリが含まれるかにかかわらず、つねにリフェッチしたい場合に適した手法です。
選択的なリフェッチ
開発では、むやみにリフェッチするのでなく、適切なクエリが再取得されていることを確かめたいでしょう。リフェッチする前に各クエリに処理を挟むには、onQueryUpdatedコールバックを指定してください。
const results = await client.refetchQueries({
updateCache(cache) {
cache.evict({ fieldName: "someRootField" });
},
onQueryUpdated(observableQuery) {
// 開発ではログをとったりdebuggerブレークポイントを加えると
// client.refetchQueriesが何をしているのか知るのに有効
console.log(`Examining ObservableQuery ${observableQuery.queryName}`);
debugger;
// onQueryUpdatedが与えられていない場合と同じ
// デフォルトのリフェッチ動作を続行
return true;
},
});
results.forEach(result => {
// ネットワークからすべてのリフェッチが実行されたあと
// 結果はApolloQueryResult<any>オブジェクトになる
});
このコード例では、追加したonQueryUpdatedが、client.refetchQueriesのリフェッチ動作をとくに変えていないことにご注意ください。onQueryUpdatedは純粋に診断やデバッグの目的で使われているのです。
特定のクエリをリフェッチから外したいときは、onQueryUpdatedからfalseを返します。
await client.refetchQueries({
updateCache(cache) {
cache.evict({ fieldName: "someRootField" });
},
onQueryUpdated(observableQuery) {
console.log(`Examining ObservableQuery ${
observableQuery.queryName
} whose latest result is ${JSON.stringify(result)} which is ${
complete ? "complete" : "incomplete"
}`);
if (shouldIgnoreQuery(observableQuery)) {
return false;
}
// ネットワークから無条件にクエリを再取得
return true;
},
});
onQueryUpdatedの第1引数ObservableQueryから得られる情報では足りないときに用いるのが、第2引数のCache.DiffResultオブジェクトです。クエリの最新の結果(result)と併せて、完了(complete)および欠落(missing)しているフィールドの情報が調べられます。
await client.refetchQueries({
updateCache(cache) {
cache.evict({ fieldName: "someRootField" });
},
onQueryUpdated(observableQuery, { complete, result, missing }) {
if (shouldIgnoreQuery(observableQuery)) {
return false;
}
if (complete) {
// ネットワークから無条件に再取得するのではなく
// 選択したFetchPolicyにしたがってクエリを更新
return observableQuery.reobserve();
}
// ネットワークから無条件にクエリを再取得
return true;
},
});
onQueryUpdatedはクエリには、動的にフィルタリングする機能があります。そのため、前述のincludeオプションと組み合わせてもよいでしょう。
await client.refetchQueries({
// デフォルトではすべてのアクティブなクエリを含む
// この指定は`onQueryUpdated`と組み合わせてクエリをフィルタリングすることが推奨
include: "active";
// アクティブなクエリに対して一度だけ呼び出され、動的にフィルタリングができる
onQueryUpdated(observableQuery) {
return !shouldIngoreQuery(observableQuery);
},
});
リフェッチのエラーを扱う
前掲コード例では、await client.refetchQueries(...)が、リフェッチされたすべてのクエリに対する最終的なApolloQueryResult<any>の結果を取得します。このPromiseの結合を行うのはPromise.allです。すると、ひとつの失敗がPromise<TResolved[]>全体の拒否につながり、他の成功した結果は隠されてしまうかもしれません。これが問題となるときは、Promise.allを待つ(await)のでなく(あるいはそれに加えて)client.refetchQueriesから返されるqueriesとresultsの配列が使えます。
const { queries, results } = client.refetchQueries({
// client.refetchQueriesオプションの指定は省略
});
const finalResults = await Promise.all(
results.map((result, i) => {
return Promise.resolve(result).catch(error => {
console.error(`Error refetching query ${queries[i].queryName}: ${error}`);
return null; // Promiseの拒否を無効にする
});
})
});
将来は、client.refetchQueriesメソッドに渡すリフェッチオプションは追加される可能性があります。同じように、戻り値のリフェッチ結果オブジェクトにも、queriesとresultsのほかにPromise関連のプロパティが加えられるかもしれません。
Apollo Client公式サイトでは、client.refetchQueriesに加えることが役立つリフェッチオプションやリフェッチ結果の候補については、気軽にIssuesを加え、ユースケースについてDiscussionsて゜説明してほしいと推奨しています。
対応するclient.mutateのオプション
変更後のリフェッチについて、client.mutateはclient.refetchQueriesと同様のオプションをサポートします(client.mutateについては「useMutationフックを使う」参照)。変更処理中の特定のタイミングでリフェッチロジックが発生することは重要です。その場合は、client.refetchQueriesでなくclient.mutateをお使いください。
過去の経緯から、client.mutateのオプションは新しいclient.refetchQueriesと名前が少し異なっています。けれど、その内部実装は実質的に同じです。つぎの表が両者の対応を示します。
client.mutate(options) |
client.refetchQueries(options) |
|
|---|---|---|
options.refetchQueries |
⇔ | options.include |
options.update |
⇔ | options.updateCache |
options.onQueryUpdated |
⇔ | options.onQueryUpdated |
options.awaitRefetchQueries |
⇔ |
onQueryUpdatedが返すPromise
|