Graphcool、React、Apolloを使って、GraphQLのSubscriptionsを実装する

## はじめに
[Graphcool、React、Apolloを使って、GraphQLのQueryとMutationを実装する](https://qiita.com/yasuhiro-yamada/items/e6b331cace59c4e2ce87)の続編です。

前回では、GraphQLのQueryとMutationの実装を行い、記事の投稿と一覧表示ができるようになりました。

但し、前回までの実装では、投稿後にその投稿した記事を同じ画面で続けて表示したい場合は、ブラウザを手動でリロードさせなければならず、使い勝手の良いUXにはなっていませんでした。

そこで今回は、GraphQLの`Subscriptions`という機能を用いてリアルタイムに記事が反映されるよう、前回作ったアプリを改修したいと思います。

縦横比が合っておらず、解像度も低い状態で恐縮ですが…、動画で実装後のイメージを載せました。

### フォームから新規投稿するイメージ
![新規投稿.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/199911/25c64bff-0deb-c763-af37-aa0695cdf94f.gif)

### 別ドメインのGraphcoolのコンソールからデータ追加し、Subscriptionsによってフォームに自動反映されるイメージ
![新規投稿2.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/199911/8271f285-b36e-9dac-a3ad-86c5fe81f7a3.gif)

### 別ドメインのGraphcoolからデータ更新（編集）して、フォームに自動反映されるイメージ
![編集.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/199911/03a22c72-bbda-b98f-661e-0da6ba6d0181.gif)

## Subscriptionsについて
`subscription`という英単語を辞書で引いてみると、`購読`が一つの訳として当てはまりますが、GraphQLの`Subscriptions`に関しては、GraphQLのサーバー側で扱うモデルのデータに変化が生じる度にプッシュし、クライアント側でその変化をリアルタイムに検知して、変化後のデータを自由に扱うことができる機能、または仕組みと言えるかと思います。

## 実装
### Web Socket接続用ライブラリのインストール
GraphQLサーバーへの接続用クライアントライブラリとして、前回でも用いたApolloを使いますが、ApolloはWeb Socketに基づいてSubscriptionsを実装していますので、ApolloがWeb Socketを扱えるように、次の２つのパッケージをインストールします。

```
yarn add apollo-link-ws subscriptions-transport-ws
```

### Web Socketリンクの作成
Subscriptions用のAPIエンドポイントを用いて、Web Socketリンクを作ります。

```react-graphcool-sample/src/index.js
const SUBSCRIPTIONS_ENDPOINT = "wss://subscriptions.graph.cool/v1/xxxxxxxxxxx";
const wsLink = new WebSocketLink({
  uri: SUBSCRIPTIONS_ENDPOINT,
  options: {
    reconnect: true
  }
});
```

前回の記事でGraphcool CLIを導入済みの方は、`graphcool init`によって作成されたディレクトリ内に移動し、`graphcool info`コマンドを実行することによって、エンドポイントを確認することができます。

```sh
graphcool info

Service Name Cluster / Service ID
──────────── ────────────────────────────────────────────
server       shared-eu-west-1/xxxxxxxx
 
API:           Endpoint:
────────────── ────────────────────────────────────────────────────────────
Simple         https://api.graph.cool/simple/v1/xxxxxxxx
Relay          https://api.graph.cool/relay/v1/xxxxxxxx
Subscriptions  wss://subscriptions.graph.cool/v1/xxxxxxxx
File           https://api.graph.cool/file/v1/xxxxxxxx
```

### `split`関数によるリンクの使い分け
Web Socketリンクに、前回から使っていたHttpリンクとあわせて、GraphQLの操作に応じて、`split`関数を用いてこの２つのリンクを使い分けます。
`getMainDefinition(query)`が`kind`、`operation`をそれぞれキーとした要素を持つオブジェクトを返すので、オブジェクトの分割代入によって`kind`と`operation`にそれぞれ値を格納し、GraphQLの操作が`subscription`かそうでないかを判別しています。
判別した結果によって、`link`に格納する値（`wsLink`または`httpLink`）を決めています。

```react-graphcool-sample/src/index.js
// 発行されるqueryの種類によって、接続先を切り替える
// split()は第一引数がtrueならば第二引数を、falseならば第三引数を返す
const link = split(
  ({ query }) => {
    const { kind, operation } = getMainDefinition(query);
    return kind === 'OperationDefinition' && 
           operation === 'subscription';
  },
  wsLink,
  httpLink,
);
```

### Subscription向けqueryを生成
`Post`というモデルに対して、記事の新規作成（CREATED）と、記事の更新（UPDATED）に関する情報をリアルタイムに受け取るには、次のようなqueryを発行します。

```react-graphcool-sample/src/components/PostList.js
const POSTS_SUBSCRIPTION = gql`
  subscription updatePost {
    Post(
      filter: {
        mutation_in: [CREATED, UPDATED] # 購読対象のmutationの種類を指定
      }
    ) {
      mutation # 実行されたmutationの種類を出力
      node { # 変化後のデータを出力
        id
        title
        content
      }
      updatedFields # UPDATEされたフィールドを出力
      previousValues { # 変化前のデータを出力
        id
        title
        content
      }
    }
  }
`;
```

こちらの実行は、Graphcoolコンソールのplaygroundで試すことができます。

### PostListの再実装
上記query（`POSTS_SUBSCRIPTION`）をPostListコンポーネントに組み込みます。

データの変化を購読（自動検知）するために、`props.allPostsQuery`の中に含まれる`subscribeToMore`という関数を、コンポーネントがレンダリングされた直後に一度だけ実行される`componentDidMount`内で実行します。

`subscribeToMore()`の引数には、`document`、`updateQuery `をそれぞれキーとして持つ要素が格納されたオブジェクトを指定します。

`document`の値には上記`POSTS_SUBSCRIPTION`を当てはめ、`updateQuery `の値には、POSTS_SUBSCRIPTIONが実行される度に実行されるコールバック関数を指定します。

```react-graphcool-sample/src/components/PostList.js
class PostList extends Component {
  componentDidMount() {
    this.props.allPostsQuery.subscribeToMore({
      document: POSTS_SUBSCRIPTION,
      updateQuery: (prev, { subscriptionData }) => {
        let result = { allPosts: prev.allPosts };
        
        if (subscriptionData.data.Post.mutation === "CREATED") {
          const newPosts = [
            ...prev.allPosts,
            subscriptionData.data.Post.node,
          ];
          console.log('newPosts', newPosts);
          result = { allPosts: newPosts };
        }
        console.log('prev', prev);
        console.log('sub', subscriptionData.data);        

        return result;
      },
    });
  }
```

UPDATEDの場合にはprev.allPostsの中身が更新されているので、それをそのままreturnしていますが、CREATEDの場合には更新されておらず、新規作成されたデータを`subscriptionData.data.Post.node`で取り出し、prev.allPostsの中身にマージしたものをreturnしています。

`prev`、`sub`および`newPosts`のそれぞれの値には何が格納されているのか、console.log()によって出力していますので、是非確認してみてください。

## Subscription実装後のソース
https://github.com/galoi/react-graphcool-sample

クローンもしくはダウンロード後、下記手順にてアプリを起動することができます。

1. プロジェクトのルートで`yarn`を実行して、パッケージをインストール
2. src/index.jsにて、2つのエンドポイントを設定
3. `yarn start`を実行して、アプリを起動

## 参考情報
- [APOLLO Subscriptions](https://www.apollographql.com/docs/react/advanced/subscriptions.html)
- [APOLLO Directional Composition](https://www.apollographql.com/docs/link/composition.html#directional)
- [How to build a Real-Time Chat with GraphQL Subscriptions and Apollo](https://blog.graph.cool/how-to-build-a-real-time-chat-with-graphql-subscriptions-and-apollo-d4004369b0d4)