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
|