Apollo Clientでwebsocket通信してみよう

## はじめに

この記事は2020年のRevCommアドベントカレンダー12日目の記事です。11日目は @hiratake55 さんの [RevCommにおけるリモート開発を支える取り組みを紹介します](https://qiita.com/hiratake55/items/c37585d93138781e940a) でした。

こんにちは、フロントエンドエンジニアの山田です。
普段の業務では**React**を使ってアプリケーション開発をしています。

本日のお題は「**Apollo Clientでwebsocket通信してみよう**」です。[Day 5](https://qiita.com/mojaja/items/c2ea0e42be4fb022fc8b)でApollo Server構築の記事がありますので、こちら使ってフロントの作成を試みます。

### 今回のアプリケーションでできること
- データの取得
- データの作成
- データのサブスクライブ

![ezgif-2-86200b7e4e63.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116850/bde9fa56-5ed2-679d-848d-6b734c199bbc.gif)


## 目次
1. 準備
1. Apollo Clientの作成
1. Queryを定義
1. 描画
1. まとめ

## 準備
さて、今回はReactを使っていきます。

```
npx create-react-app apollo-client
```
必要なパッケージをインストール

```
yarn add @apollo/client subscriptions-transport-ws
```

## Apollo Clientの作成

GraphQLとのやりとりをすべてWebSocketで行うので、エンドポイントを`ws://localhost:4000/graphql`とします。
`SubscriptionsClient`インスタンスをネットワークインターフェイスとして使用し`ApolloClient`インスタンスを作成します。

```/src/apollo/client.js
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from '@apollo/client/link/ws';

const cache = new InMemoryCache();
const wsClient = new SubscriptionClient('ws://localhost:4000/graphql', {
  reconnect: true,
});

const wsLink = new WebSocketLink(wsClient);

export const client = new ApolloClient({
  cache,
  link: wsLink,
});
```
ApolloProviderをWrapしていきます。

```/src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloProvider } from '@apollo/client';
import App from './App';
import { client } from './apollo/client';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </React.StrictMode>,
  document.getElementById('root')
);// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```

## Queryを定義
バックエンドをもとにgraphqlのqueryを作成します。

```/src/apollo/query/book.js
import { gql } from '@apollo/client';

export const GET_BOOKS = gql`
  query books {
    books {
      title
      author
    }
  }
`;

export const ADD_BOOKS = gql`
  mutation AddBook($title: String!, $author: String!) {
    addBook(title: $title, author: $author) {
      title
      author
    }
  }
`;

export const SUBSCRIBE_BOOKS = gql`
  subscription {
    bookAdded {
      title
      author
    }
  }
`;
```

## 描画

最後の仕上げです。
ここで大事なのは`subscribeToMore`を使ってデータの監視を行うことです。


- **document**:は`subscription`のクエリを設定します。
- **updateQuery**はクエリの現在キャッシュされている結果（prev）をGraphQLサーバーによってプッシュされたsubscriptionDataと組み合わせる方法をApolloクライアントに指示する関数です。この関数の戻り値は、クエリの現在のキャッシュ結果を完全に置き換えます。

今回の例では`useMutation`で作成されたデータを`subscribeToMore`で監視し、`prev`のデータに追加することでリストの更新が行われる仕組みとなっています。

```/src/App.jsx
import React, { useEffect, useState } from 'react';
import { useMutation, useLazyQuery } from '@apollo/client';

import { GET_BOOKS, ADD_BOOKS, SUBSCRIBE_BOOKS } from './apollo/query/book';

const App = () => {
  const [author, setAuthor] = useState('');
  const [title, setTitle] = useState('');
  const [getBooks, { data, called, subscribeToMore, refetch }] = useLazyQuery(GET_BOOKS);
  const [addBook] = useMutation(ADD_BOOKS);

  useEffect(() => {
    if (!called) {
      getBooks();
    }
  }, [called, getBooks]);

  useEffect(() => {
    if (refetch && called) {
      refetch();
    }
  }, [called, refetch]);

  useEffect(() => {
    if (subscribeToMore) {
      subscribeToMore({
        document: SUBSCRIBE_BOOKS,
        updateQuery: (prev, { subscriptionData }) => {
          if (!subscriptionData.data) return prev;
          const newBook = subscriptionData.data.bookAdded;
          const updatedBooks = [...prev.books, newBook];
          return { books: updatedBooks }
        }
      });
    }
  }, [subscribeToMore]);

  const handleAuthorChange = (e) => {
    setAuthor(e.target.value);
  };

  const handleTitleChange = (e) => {
    setTitle(e.target.value);
  };

  const handleSubmit = () => {
    addBook({ variables: { title, author }});
    setAuthor('');
    setTitle('');
  };

  return (
    <div style={{ padding: 20 }}>
      <div style={{ display: "flex", alignItems: "flex-end" }}>
        <div>
          <p style={{ margin: 0 }}>タイトル</p>
          <input type="text" name="title" value={title} onChange={handleTitleChange} />
        </div>
        <div style={{ marginLeft: 20 }}>
          <p style={{ margin: 0 }}>著者名</p>
          <input type="text" name="author" value={author} onChange={handleAuthorChange} />
        </div>
        <div style={{ marginLeft: 10 }}>
          <button style={{ cursor: "pointer" }} type="submit" onClick={handleSubmit}>登録</button>
        </div>
      </div>
      {data && (
        <ul>
          {data.books.map((book, i) => {
            return (
              <li key={i}>{`${book.title} (${book.author})`}</li>
            )
          })}
        </ul>
      )}
    </div>
  );
}

export default App;
```

## まとめ
いかがでしたでしょうか？
今回は**Apollo Client**でwebsocket通信をしてみました。

Revcommで一緒に働いてくれる方を絶賛募集しています。
募集中のポジションは、[株式会社RevComm 採用情報](https://hrmos.co/pages/revcomm/jobs) をご覧ください。

次回は@shintaromatsudoのさんの記事になります。