React + TypeScript: Apollo ClientのサブスクリプションによりGraphQLサーバーからリアルタイムに更新を取得する

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

GraphQLが[クエリ](https://qiita.com/FumioNonaka/items/eafa971127b1a20438fb)と[変更](https://qiita.com/FumioNonaka/items/4a1b474c16de46254e60)に加えてサポートする3つめの操作がサブスクリプションです。

クエリと同じく、サブスクリプションはデータをフェッチできます。クエリと違うのは、サブスクリプションが長期間にわたる操作だということです。時間の経過とともに結果を変更できます。GraphQLサーバーとのアクティブな接続が維持されるのです(もっとも一般的なのはWebSocket経由)。サブスクリプションの結果に、サーバーから更新をプッシュできます。

バックエンドデータの変更をリアルタイムでクライアントに通知できるのが、サブスクリプションの便利な点です。新しいオブジェクトの作成や重要なフィールドの更新などが考えられます。

# サブスクリプションをいつ使うか
ほとんどの場合、クライアントがサブスクリプションによりバックエンドの最新情報を取得することはお勧めできません。[`pollInterval`](https://www.apollographql.com/docs/react/api/react/hooks/#pollinterval)を使えば一定の時間間隔で問い合わせできます。あるいは、[`refetch`](https://www.apollographql.com/docs/react/api/react/hooks/#refetch)関数により、何らかのアクション(たとえば、ボタンクリック)に応じて、必要なクエリを再実行してもよいでしょう(「[キャッシュしたクエリの結果を更新する](https://qiita.com/FumioNonaka/items/eafa971127b1a20438fb#%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%81%97%E3%81%9F%E3%82%AF%E3%82%A8%E3%83%AA%E3%81%AE%E7%B5%90%E6%9E%9C%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B)」参照)。

サブスクリプションを用いるのは、つぎのような場合です。

* **大きなオブジェクトの小さく段階的な変更**  
大きなオブジェクトを繰り返し更新するのは、とくにそのフィールドのほとんどがほぼ変わらない場合は高コストです。そのようなとき、オブジェクトの初期状態はクエリで取得しておき、サーバーから個々のフィールドの更新をプロアクティブにプッシュできます。
* **遅延の少ないリアルタイムの更新**  
たとえば、チャットアプリケーションのクライアントは、新しいメッセージが入手可能になり次第、それを受け取りたいでしょう。


# サブスクリプションライブラリを選ぶ
GraphQLの仕様には、サブスクリプションリクエストを送るための特定のプロトコルは定義されていません。WebSocke上でサブスクリプションを実装したApollo Clientがサポートするライブラリには、[graphql-ws](https://github.com/enisdenjo/graphql-ws)があります。このあとご説明するのは、graphql-wsの使い方です。


# サブスクリプションを定義する
クエリや変更と同じく、サブスクリプションはサーバーサイドとクライアントサイドの両方で定義します。

## サーバーサイド
GraphQLスキーマで使用可能なサブスクリプションを`Subscription`型で定めてください。つぎの`commentAdded`は、特定のブログ記事(`postID`で指定)に新しいコメントが加えられるたびにサブスクライブしているクライアントに通知するサブスクリプションです。

```graphql
type Subscription {
	commentAdded(postID: ID!): Comment
}
```

サーバー側でサブスクリプションのサポートを実装する方法について、詳しくは「[Subscriptions in Apollo Server](https://www.apollographql.com/docs/apollo-server/data/subscriptions/)」をご参照ください。

## クライアントサイド
アプリケーションのクライアントでは、Apollo Clientで実行する各サブスクリプションの形状を、つぎのように定めてください。

```javascript
const COMMENTS_SUBSCRIPTION = gql`
	subscription OnCommentAdded($postID: ID!) {
		commentAdded(postID: $postID) {
			id
			content
		}
	}
`;
```

Apollo Clientが`OnCommentAdded`サブスクリプションを実行すると、GraphQLサーバーへの接続が確立され、応答データを待ち受けます。クエリとは異なり、サーバーが直ちに処理して応答を返すわけではありません。サーバーがデータをクライアントにプッシュするのは、バックエンドで特定のイベントが発生したときのみです。

GraphQLサーバーがデータをサブスクライブしているクライアントにプッシュするときはつねに、データはクエリの場合と同じく実行されたサブスクリプションの構造にしたがいます。

```json
{
	"data": {
		"commentAdded": {
			"id": "123",
			"content": "What a thoughtful and well written post!"
		}
	}
}
```


# トランスポートを設定する
サブスクリプションは通常、つねに接続を保つため、ApolloClientがクエリと変更に用いるデフォルトのHTTPトランスポートは使わないでください。 Apolloクライアントサブスクリプションでもっとも一般的なのは、[graphql-ws](https://github.com/enisdenjo/graphql-ws#readme)ライブラリを介してWebSocketで通信することです。

## 1. 必要なライブラリをインストールする
[Apollo Link](https://www.apollographql.com/docs/react/api/link/introduction)は、Apolloクライアントのネットワーク通信をカスタマイズするのに役立つライブラリです。 このライブラリで**リンクチェーン**を定義すれば、操作を変更したり、適切な宛先にルーティングできます。

WebSocketを介してサブスクリプションを実行するため、リンクチェーンに加えるのが`GraphQLWsLink`です。 このリンクに必要な`graphql-ws`ライブラリを、つぎのようにインストールしてください。

```terminal
npm install graphql-ws
```

## 2. `GraphQLWsLink`を初期化する
`ApolloClient`を初期化した同じプロジェクトファイルに、`GraphQLWsLink`オブジェクトをインポートして初期化します。

```javascript:index.js
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

const wsLink = new GraphQLWsLink(createClient({
	url: 'ws://localhost:4000/subscriptions',
}));
```

`url`オプションの値は、GraphQLサーバーのサブスクリプション固有のWebSocketエンドポイントに書き替えます。 Apollo Serverをお使いの場合の設定は、「Subscriptions in Apollo Server」の「[Enabling subscriptions](https://www.apollographql.com/docs/apollo-server/data/subscriptions/#enabling-subscriptions)」をご参照ください。

## 操作によって通信を分ける(推奨)
Apolloクライアントは、`GraphQLWsLink`を用いてすべてのタイプの操作が実行できます。けれどほとんどの場合、クエリと変更には引き続きHTTPを使うのがよいでしょう。 クエリと変更はステートフルでなく、長時間接続し続ける必要もありません。WebSocket接続がまだされていないときに、HTTPをより効率的かつスケーラブルにできるからです。

通信の切り分けをサポートするために、`@apollo/client`ライブラリには`split`関数が備わっています。ブール値で確かめた結果に応じて、ふたつの異なる`Link`のうちのひとつが使えるのです。

つぎのコードは、前の例を拡張して、`GraphQLWsLink`と`HttpLink`の両方を初期化しました。 そして、`split`関数を使用して、ふたつの`Link`をひとつにまとめています。ふたつのうちどちらを使うかは、実行される操作のタイプに応じて変わるのです。

```javascript:index.js
import { split, HttpLink } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

const httpLink = new HttpLink({
	uri: 'http://localhost:4000/graphql'
});

const wsLink = new GraphQLWsLink(createClient({
	url: 'ws://localhost:4000/subscriptions',
}));

// split関数の3つの引数
// [1]各操作が実行されるたびに呼び出される関数
// [2]関数の戻り値が真のとき操作に用いるリンク
// [3]関数の戻り値が偽のとき操作に用いるリンク
const splitLink = split(
	({ query }) => {
		const definition = getMainDefinition(query);
		return (
			definition.kind === 'OperationDefinition' &&
			definition.operation === 'subscription'
		);
	},
	wsLink,
	httpLink,
);
```

このロジックによって、クエリと変更は通常どおりHTTP(`httpLink`)を用い、サブスクリプションはWebSocket(`wsLink`)を使います。

## 4. Apollo Clientにリンクチェーンを与える
ふたつの`Link`をまとめたリンクチェーン(`splitLink`)を定義したら、`Link`コンストラクタオプションでApollo Clientに与えます。

```javascript:index.js
import { ApolloClient, InMemoryCache } from '@apollo/client';

// ...[前掲コードは省略]...

const client = new ApolloClient({
	link: splitLink,
	cache: new InMemoryCache()
});
```

[注記] `link`オプションの指定は、`uri`オプションよりも優先されます(`uri`に与えたURLが用いられるのはデフォルトのHTTPリンクチェーンとしてです)。

## WebSocketで認証する(オプション)
サブスクリプション結果の受信は、クライアントを認証してから許可すべきことが少なくありません。 そのためにはつぎのように、`graphQLWsLink`コンストラクターに`connectionParams`オプションを与えます。

```javascript
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';

const wsLink = new GraphQLWsLink(createClient({
	url: 'ws://localhost:4000/subscriptions',
	connectionParams: {
		authToken: user.authToken,
	},
}));
```

`GraphQLWsLink`は、接続するたびに`connectionParams`オブジェクトをサーバーに渡します。 サーバーは受け取った`connectionParams`オブジェクトを用いて、認証や他の接続関連のタスクが実行されるのです。


# サブスクリプションを実行する
Reactからサブスクリプションを実行するために用いるのが、Apollo Clientの`useSubscription`フックです。 `useQuery`と同じく、`useSubscription`はApollo Clientから結果オブジェクトを返します。その中に収められた`loading`や`error`および`data`プロパティをUIのレンダリングに使うのです。

つぎのコードのコンポーネント(`LatestComment`)は、前の例ですでに定義したサブスクリプションを使います。指定したブログ投稿に加えられた最新のコメントをレンダリングするためです。 GraphQLサーバーが新しいコメントをクライアントにプッシュするたびに、コンポーネントは新しいコメントとともに再描画されます。

```javascript
const COMMENTS_SUBSCRIPTION = gql`
	subscription OnCommentAdded($postID: ID!) {
		commentAdded(postID: $postID) {
			id
			content
		}
	}
`;

function LatestComment({ postID }) {
	const { data, loading } = useSubscription(
		COMMENTS_SUBSCRIPTION,
		{ variables: { postID } }
	);
	return <h4>New comment: {!loading && data.commentAdded.content}</h4>;
}
```


# クエリの更新をサブスクライブする
ApolloClientでクエリが結果を返すとき、そこに必ず含まれるのが`subscribeToMore`関数です。 この関数でフォローアップサブスクリプションを実行すると、クエリのもとの結果に更新がプッシュできます。

[注記] `subscribeToMore`関数の構造は、ページネーション処理によく使われる`fetchMore`関数と似ています。 おもな違いは実行する操作で、`fetchMore`がフォローアップクエリなのに対し、`subscribeToMore`はサブスクリプションだということです。

まずは、標準的なクエリのコード例から始めましょう。特定のブログ記事への既存のコメントをすべて取得します。

```javascript
const COMMENTS_QUERY = gql`
	query CommentsForPost($postID: ID!) {
		post(postID: $postID) {
			comments {
				id
				content
			}
		}
	}
`;

function CommentsPageWithData({ params }) {
	const result = useQuery(
		COMMENTS_QUERY,
		{ variables: { postID: params.postID } }
	);
	return <CommentsPage {...result} />;
}
```

さてここで、記事に新たなコメントが加えられたら、すぐにGraphQLサーバーからクライアントに更新をプッシュさせたいとしましょう。まず必要なのは、サブスクリプションの定義です。`COMMENTS_QUERY`が返ってきたら、Apollo Clientにこのサブスクリプションを実行させます。

```javascript
const COMMENTS_SUBSCRIPTION = gql`
	subscription OnCommentAdded($postID: ID!) {
		commentAdded(postID: $postID) {
			id
			content
		}
	}
`;
```

つぎに、`CommentsPageWithData`関数の修正です。戻り値の`CommentsPage`コンポーネントに`subscribeToNewComments`プロパティを加えます。このプロパティに定めるのは、`subscribeToMore`を実行する関数です。このあと、コンポーネントがマウントされたら(`componentDidMount()`)関数を呼び出すようにします。

```javascript
function CommentsPageWithData({ params }) {
	const { subscribeToMore, ...result } = useQuery(
		COMMENTS_QUERY,
		{ variables: { postID: params.postID } }
	);

	return (
		<CommentsPage
			{...result}
			subscribeToNewComments={() =>
				subscribeToMore({
					document: COMMENTS_SUBSCRIPTION,
					variables: { postID: params.postID },
					updateQuery: (prev, { subscriptionData }) => {
						if (!subscriptionData.data) return prev;
						const newFeedItem = subscriptionData.data.commentAdded;

						return Object.assign({}, prev, {
							post: {
								comments: [newFeedItem, ...prev.post.comments]
							}
						});
					}
				})
			}
		/>
	);
}
```

このコード例で、`subscribeToMore`の引数オブジェクトに与えたオプションはつぎの3つです。

* `document` - 実行するサブスクリプション。
* `variables` - サブスクリプションを実行するときに含める変数。
* `updateQuery` - クエリの結果をどうまとめるかApollo Clientに指示する関数。
    - 引数 - オプションオブジェクト。
        - `prev` - クエリの現在キャッシュされている結果。
        - `subscriptionData` - GraphQLサーバーからプッシュされたデータ。
    - 戻り値 - クエリの現在のキャッシュ結果を**完全に上書きする**値。

最後に、クラスコンポーネント`CommentsPage`の定義で、コンポーネントがマウントされたとき(`componentDidMount()`)に`subscribeToNewComments`を実行するよう定めます。

```javascript
export class CommentsPage extends Component {
	componentDidMount() {
		this.props.subscribeToNewComments();
	}
}
```


# `useSubscription`APIリファレンス

[注記] React Apolloの`Subscription`レンダープロップコンポーネントを使った場合も、以下の表のオプション/結果の内容はそのまま有効です(オプションはコンポーネントプロップで、結果はレンダープロップ関数に渡されます)。 ひとつだけ違うのは、`subscription`プロップ(`gql`がASTに解析したGraphQLサブスクリプションドキュメントを保持する)も必要なことです。

レンダープロップコンポーネントには、`Subscription`に加えて、`Query`と`Mutation`があります。けれど、「[Extending components](https://www.apollographql.com/docs/react/development-testing/static-typing/#extending-components)」によれば、関数コンポーネントとともにそれぞれ`useSubscription`と`useQuery`、および`useMutation`を用いるのがこれからの推奨です。

## オプション

| オプション | 型 | 説明 |
| --- | --- | --- |
| `subscription` |	DocumentNode | `graphql-tag`がASTに解析したGraphQLサブスクリプションドキュメント。`useSubscription`フックでは、省略できる。サブスクリプションはフックの第1引数として渡せるため。`Subscription`コンポーネントでは必須。 |
| `variables` |	{ [key: string]: any } | サブスクリプションが実行する必要のあるすべての変数を含むオブジェクト。 |
| `shouldResubscribe` |	boolean | サブスクリプションを解除して再度サブスクライブする必要があるかどうか。 |
| `onSubscriptionData` |	(options: OnSubscriptionDataOptions<TData>) => any | `useSubscription`フック/`Subscription`コンポーネントがデータを受け取るたびに呼び出されるコールバック関数の登録先。コールバックの`options`オブジェクトは、`client`内の現在のApollo Clientインスタンスのパラメータ、および`subscriptionData`で受け取ったサブスクリプションデータにより構成される。 |
| `fetchPolicy` |	FetchPolicy | コンポーネントがApolloキャッシュとどのように相互作用するかを指定する(「[Setting a fetch policy](https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy)」および「[フェッチポリシーをカスタマイズする](https://apollographql-jp.com/tutorial/queries/#%E3%83%95%E3%82%A7%E3%83%83%E3%83%81%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC%E3%82%92%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA%E3%81%99%E3%82%8B)」参照)。 |
| `context` |	Record<string, any> | コンポーネントとネットワークインタフェース(Apollo Link)間の共有コンテクスト。 |
| `client` |	ApolloClient | `ApolloClient`インスタンス。デフォルトでは、`useSubscription`/`Subscription`は、コンテクストから渡されたクライアントを用いる。別のクライアントを渡すことも可能。 |


## 結果
`useSubscription`フックは、呼び出されたあと、つぎの表のプロパティが備わった結果オブジェクトを返します。

| プロパティ | 型 | 説明 |
| --- | --- | --- |
| `data` |	TData | GraphQLサブスクリプションの結果を含むオブジェクト。デフォルトは空のオブジェクト。 |
| `loading` |	boolean | 初期データが返されたかどうかを示すブール値。 |
| `error` |	ApolloError | `graphQLErrors`および`networkError`プロパティで発生したランタイムエラー。 |