Apollo ClientはReactで使える状態管理ライブラリです。ローカルとリモートのデータをGraphQLで扱えます。本稿は公式サイトの「Subscriptions - Get real-time updates from your GraphQL server」にもとづいて、GraphQLサーバーからデータをリアルタイムにどう更新するかについての解説です。Apollo Clientでクエリを使うための基礎はすでに学んだことが前提となります(まだの方は先に「React + TypeScript: Apollo ClientのGraphQLクエリを使ってみる」をお読みください)。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。
GraphQLがクエリと変更に加えてサポートする3つめの操作がサブスクリプションです。
クエリと同じく、サブスクリプションはデータをフェッチできます。クエリと違うのは、サブスクリプションが長期間にわたる操作だということです。時間の経過とともに結果を変更できます。GraphQLサーバーとのアクティブな接続が維持されるのです(もっとも一般的なのはWebSocket経由)。サブスクリプションの結果に、サーバーから更新をプッシュできます。
バックエンドデータの変更をリアルタイムでクライアントに通知できるのが、サブスクリプションの便利な点です。新しいオブジェクトの作成や重要なフィールドの更新などが考えられます。
サブスクリプションをいつ使うか
ほとんどの場合、クライアントがサブスクリプションによりバックエンドの最新情報を取得することはお勧めできません。pollInterval
を使えば一定の時間間隔で問い合わせできます。あるいは、refetch
関数により、何らかのアクション(たとえば、ボタンクリック)に応じて、必要なクエリを再実行してもよいでしょう(「キャッシュしたクエリの結果を更新する」参照)。
サブスクリプションを用いるのは、つぎのような場合です。
-
大きなオブジェクトの小さく段階的な変更
大きなオブジェクトを繰り返し更新するのは、とくにそのフィールドのほとんどがほぼ変わらない場合は高コストです。そのようなとき、オブジェクトの初期状態はクエリで取得しておき、サーバーから個々のフィールドの更新をプロアクティブにプッシュできます。 -
遅延の少ないリアルタイムの更新
たとえば、チャットアプリケーションのクライアントは、新しいメッセージが入手可能になり次第、それを受け取りたいでしょう。
サブスクリプションライブラリを選ぶ
GraphQLの仕様には、サブスクリプションリクエストを送るための特定のプロトコルは定義されていません。WebSocke上でサブスクリプションを実装したApollo Clientがサポートするライブラリには、graphql-wsがあります。このあとご説明するのは、graphql-wsの使い方です。
サブスクリプションを定義する
クエリや変更と同じく、サブスクリプションはサーバーサイドとクライアントサイドの両方で定義します。
サーバーサイド
GraphQLスキーマで使用可能なサブスクリプションをSubscription
型で定めてください。つぎのcommentAdded
は、特定のブログ記事(postID
で指定)に新しいコメントが加えられるたびにサブスクライブしているクライアントに通知するサブスクリプションです。
type Subscription {
commentAdded(postID: ID!): Comment
}
サーバー側でサブスクリプションのサポートを実装する方法について、詳しくは「Subscriptions in Apollo Server」をご参照ください。
クライアントサイド
アプリケーションのクライアントでは、Apollo Clientで実行する各サブスクリプションの形状を、つぎのように定めてください。
const COMMENTS_SUBSCRIPTION = gql`
subscription OnCommentAdded($postID: ID!) {
commentAdded(postID: $postID) {
id
content
}
}
`;
Apollo ClientがOnCommentAdded
サブスクリプションを実行すると、GraphQLサーバーへの接続が確立され、応答データを待ち受けます。クエリとは異なり、サーバーが直ちに処理して応答を返すわけではありません。サーバーがデータをクライアントにプッシュするのは、バックエンドで特定のイベントが発生したときのみです。
GraphQLサーバーがデータをサブスクライブしているクライアントにプッシュするときはつねに、データはクエリの場合と同じく実行されたサブスクリプションの構造にしたがいます。
{
"data": {
"commentAdded": {
"id": "123",
"content": "What a thoughtful and well written post!"
}
}
}
トランスポートを設定する
サブスクリプションは通常、つねに接続を保つため、ApolloClientがクエリと変更に用いるデフォルトのHTTPトランスポートは使わないでください。 Apolloクライアントサブスクリプションでもっとも一般的なのは、graphql-wsライブラリを介してWebSocketで通信することです。
1. 必要なライブラリをインストールする
Apollo Linkは、Apolloクライアントのネットワーク通信をカスタマイズするのに役立つライブラリです。 このライブラリでリンクチェーンを定義すれば、操作を変更したり、適切な宛先にルーティングできます。
WebSocketを介してサブスクリプションを実行するため、リンクチェーンに加えるのがGraphQLWsLink
です。 このリンクに必要なgraphql-ws
ライブラリを、つぎのようにインストールしてください。
npm install graphql-ws
2. GraphQLWsLink
を初期化する
ApolloClient
を初期化した同じプロジェクトファイルに、GraphQLWsLink
オブジェクトをインポートして初期化します。
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」をご参照ください。
操作によって通信を分ける(推奨)
Apolloクライアントは、GraphQLWsLink
を用いてすべてのタイプの操作が実行できます。けれどほとんどの場合、クエリと変更には引き続きHTTPを使うのがよいでしょう。 クエリと変更はステートフルでなく、長時間接続し続ける必要もありません。WebSocket接続がまだされていないときに、HTTPをより効率的かつスケーラブルにできるからです。
通信の切り分けをサポートするために、@apollo/client
ライブラリにはsplit
関数が備わっています。ブール値で確かめた結果に応じて、ふたつの異なるLink
のうちのひとつが使えるのです。
つぎのコードは、前の例を拡張して、GraphQLWsLink
とHttpLink
の両方を初期化しました。 そして、split
関数を使用して、ふたつのLink
をひとつにまとめています。ふたつのうちどちらを使うかは、実行される操作のタイプに応じて変わるのです。
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に与えます。
import { ApolloClient, InMemoryCache } from '@apollo/client';
// ...[前掲コードは省略]...
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache()
});
[注記] link
オプションの指定は、uri
オプションよりも優先されます(uri
に与えたURLが用いられるのはデフォルトのHTTPリンクチェーンとしてです)。
WebSocketで認証する(オプション)
サブスクリプション結果の受信は、クライアントを認証してから許可すべきことが少なくありません。 そのためにはつぎのように、graphQLWsLink
コンストラクターにconnectionParams
オプションを与えます。
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サーバーが新しいコメントをクライアントにプッシュするたびに、コンポーネントは新しいコメントとともに再描画されます。
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
はサブスクリプションだということです。
まずは、標準的なクエリのコード例から始めましょう。特定のブログ記事への既存のコメントをすべて取得します。
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にこのサブスクリプションを実行させます。
const COMMENTS_SUBSCRIPTION = gql`
subscription OnCommentAdded($postID: ID!) {
commentAdded(postID: $postID) {
id
content
}
}
`;
つぎに、CommentsPageWithData
関数の修正です。戻り値のCommentsPage
コンポーネントにsubscribeToNewComments
プロパティを加えます。このプロパティに定めるのは、subscribeToMore
を実行する関数です。このあと、コンポーネントがマウントされたら(componentDidMount()
)関数を呼び出すようにします。
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
を実行するよう定めます。
export class CommentsPage extends Component {
componentDidMount() {
this.props.subscribeToNewComments();
}
}
useSubscription
APIリファレンス
[注記] React ApolloのSubscription
レンダープロップコンポーネントを使った場合も、以下の表のオプション/結果の内容はそのまま有効です(オプションはコンポーネントプロップで、結果はレンダープロップ関数に渡されます)。 ひとつだけ違うのは、subscription
プロップ(gql
がASTに解析したGraphQLサブスクリプションドキュメントを保持する)も必要なことです。
レンダープロップコンポーネントには、Subscription
に加えて、Query
とMutation
があります。けれど、「Extending components」によれば、関数コンポーネントとともにそれぞれuseSubscription
とuseQuery
、およびuseMutation
を用いるのがこれからの推奨です。
オプション
オプション | 型 | 説明 |
---|---|---|
subscription |
DocumentNode |
graphql-tag がASTに解析したGraphQLサブスクリプションドキュメント。useSubscription フックでは、省略できる。サブスクリプションはフックの第1引数として渡せるため。Subscription コンポーネントでは必須。 |
variables |
{ [key: string]: any } | サブスクリプションが実行する必要のあるすべての変数を含むオブジェクト。 |
shouldResubscribe |
boolean | サブスクリプションを解除して再度サブスクライブする必要があるかどうか。 |
onSubscriptionData |
(options: OnSubscriptionDataOptions) => any |
useSubscription フック/Subscription コンポーネントがデータを受け取るたびに呼び出されるコールバック関数の登録先。コールバックのoptions オブジェクトは、client 内の現在のApollo Clientインスタンスのパラメータ、およびsubscriptionData で受け取ったサブスクリプションデータにより構成される。 |
fetchPolicy |
FetchPolicy | コンポーネントがApolloキャッシュとどのように相互作用するかを指定する(「Setting a fetch policy」および「フェッチポリシーをカスタマイズする」参照)。 |
context |
Record | コンポーネントとネットワークインタフェース(Apollo Link)間の共有コンテクスト。 |
client |
ApolloClient |
ApolloClient インスタンス。デフォルトでは、useSubscription /Subscription は、コンテクストから渡されたクライアントを用いる。別のクライアントを渡すことも可能。 |
結果
useSubscription
フックは、呼び出されたあと、つぎの表のプロパティが備わった結果オブジェクトを返します。
プロパティ | 型 | 説明 |
---|---|---|
data |
TData | GraphQLサブスクリプションの結果を含むオブジェクト。デフォルトは空のオブジェクト。 |
loading |
boolean | 初期データが返されたかどうかを示すブール値。 |
error |
ApolloError |
graphQLErrors およびnetworkError プロパティで発生したランタイムエラー。 |