React + TypeScript: Apollo Clientでクエリをリフェッチ(refetch)する

[Apollo Client](https://www.apollographql.com/docs/react/)はReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿は公式サイトの「[Refetching queries in Apollo Client](https://www.apollographql.com/docs/react/data/refetching)」にもとづいて、Apollo ClientのメソッドでクライアントサイドのGraphQLデータをどう更新するかについての解説です。Apollo Clientでクエリを使うための基礎はすでに学んだことが前提となります(まだの方は先に「[React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる](https://qiita.com/FumioNonaka/items/0c6b711627e3443ff73b)」をお読みください)。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。

Apollo Clientは、キャッシュの更新によりGraphQLデータをローカルで変更できます(「[update関数で項目データをリストに書き加える](https://qiita.com/FumioNonaka/items/4a1b474c16de46254e60#update%E9%96%A2%E6%95%B0%E3%81%A7%E9%A0%85%E7%9B%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AB%E6%9B%B8%E3%81%8D%E5%8A%A0%E3%81%88%E3%82%8B)」参照)。けれど、クライアント側のGraphQLデータは、サーバーからクエリを再取得(refetch)して更新した方が端的な場合もあるでしょう。

理屈では、クライアントサイドが更新されたら、すべてのアクティブなクエリを再取得することもできます。けれど、リフェッチするクエリを選べば、時間とネットワーク帯域幅が節約できるはずです。[`InMemoryCache`](https://www.apollographql.com/docs/react/api/cache/InMemoryCache/)は、最近のキャッシュの更新によって有効性が失われたかもしれないアクティブなクエリを特定するのに役立ちます。

ローカルキャッシュの更新をリフェッチと組み合わせると、特に効果的です。アプリケーションは、ローカルキャッシュの変更結果を直ちに表示します。同時に、バックグラウンドでリフェッチし、サーバーから最新のデータを取得することができるからです。すると、ローカルデータとリフェッチされたデータの間に差異がある場合にのみ、UIが再レンダリングされます。

リフェッチをよく行うのは変更のあとです。そのため、変更関数は[`refetchQueries`](https://www.apollographql.com/docs/react/data/mutations/#refetching-queries)や[`onQueryUpdated`](https://www.apollographql.com/docs/react/data/mutations/#refetching-after-update)などのオプションを受け取り、どのクエリをどのようにリフェッチするか指定できます(変更関数については「[useMutationフックを使う](https://qiita.com/FumioNonaka/items/4a1b474c16de46254e60#usemutation%E3%83%95%E3%83%83%E3%82%AF%E3%82%92%E4%BD%BF%E3%81%86)」参照)。

変更の外でクエリを選んでリフェッチしたいときは、`ApolloClient`の`refetchQueries`メソッドをお使いください。構文の解説はつぎのとおりです。


# `client.refetchQueries`

## リフェッチオプション
`client.refetchQueries`メソッドは、つぎのTypeScriptインタフェースに準拠したオプションオブジェクトを受け取ります。

```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`](https://www.apollographql.com/docs/react/data/mutations/#options)配列と同じ)。すべての(アクティブな)クエリをリフェッチするための省略記法が`"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`オプションを単独で用いて名前を与えます。

```graphql
await client.refetchQueries({
	include: ["SomeQueryName"],
});
```

`include`オプションに渡すのは`DocumentNode`でも、特定のクエリを再取得することが可能です。

```graphql
await client.refetchQueries({
	include: [SOME_QUERY],
});
```

### すべてのクエリをリフェッチする
アクティブなクエリをすべて再取得するには、`include`に省略表記の`"active"`を渡してください。

```graphql
await client.refetchQueries({
	include: "active",
});
```

アクティブでないクエリまですべて含めてフェッチしたいとき、`include`渡す省略表記は`"all"`です。オブザーバーをもたなかったり、コンポーネントがマウントされていないクエリまで含まれます。

```graphql
await client.refetchQueries({
	include: "all", // "active"を使うべきか判断する
});
```

### キャッシュ更新の影響を受けたクエリのリフェッチ
`updateCache`コールバックで実行されたキャッシュ更新の影響が及んだクエリを再取得することができます。

```graphql
await client.refetchQueries({
	updateCache(cache) {
		cache.evict({ fieldName: "someRootField" });
	},
});
```

リフェッチされるのは、`Query.someRootField`に依存するすべてのクエリです。どのクエリが含まれるか、あらかじめ知らなくて構いません。`updateCache`には、任意のキャッシュ操作(`writeQuery`、`writeFragment`、`modify`、`evict`など)が組み合わせられるのです。

`updateCache`が実行した更新は、デフォルトでキャッシュに残ります。一時的な楽観レイヤーで実行すれば、`client.refetchQueries`が更新を監視し終えたあと、キャッシュを変更せずにすぐに破棄することも可能です。

```graphql
await client.refetchQueries({
	updateCache(cache) {
		cache.evict({ fieldName: "someRootField" });
	},

	// 楽観レイヤーで、Query.someRootFieldを一時的に削除
	optimistic: true,
});
```

キャッシュデータを実際に変更することなく、キャッシュを「更新」するには、もうひとつやり方があります。`cache.modify`とその`INVALIDATE`センチネルオブジェクトを用いることです。

```graphql
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`コールバックを指定してください。

```javascript
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`を返します。

```graphql
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`)しているフィールドの情報が調べられます。

```graphql
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`オプションと組み合わせてもよいでしょう。

```graphql
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`の配列が使えます。

```graphql
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](https://github.com/apollographql/apollo-feature-requests/issues)を加え、ユースケースについて[Discussions](https://github.com/apollographql/apollo-client/discussions)て゜説明してほしいと推奨しています。

## 対応する`client.mutate`のオプション
変更後のリフェッチについて、`client.mutate`は`client.refetchQueries`と同様のオプションをサポートします(`client.mutate`については「[useMutationフックを使う](https://qiita.com/FumioNonaka/items/4a1b474c16de46254e60#usemutation%E3%83%95%E3%83%83%E3%82%AF%E3%82%92%E4%BD%BF%E3%81%86)」参照)。変更処理中の特定のタイミングでリフェッチロジックが発生することは重要です。その場合は、`client.refetchQueries`でなく`client.mutate`をお使いください。

過去の経緯から、`client.mutate`のオプションは新しい`client.refetchQueries`と名前が少し異なっています。けれど、その内部実装は実質的に同じです。つぎの表が両者の対応を示します。

| `client.mutate(options)` |  | `client.refetchQueries(options)` |
| --- | --- | --- |
| [`options.refetchQueries`](https://www.apollographql.com/docs/react/data/mutations/#refetching-queries) | ⇔ | `options.include` |
| [`options.update`](https://www.apollographql.com/docs/react/data/mutations/#the-update-function) | ⇔ | `options.updateCache` |
| [`options.onQueryUpdated`](https://www.apollographql.com/docs/react/data/mutations/#refetching-after-update) | ⇔ | `options.onQueryUpdated` |
| [`options.awaitRefetchQueries`](https://www.apollographql.com/docs/react/data/mutations/#awaitrefetchqueries) | ⇔ | `onQueryUpdated`が返す`Promise` |