Reactを使ってSocket.IOサーバと通信しグラフを描画

Reactの勉強をはじめたので、Socket.IOと通信するコードを参考に、取得したデータをRechartsで表示しました。
自分の理解用に作成しましたが、なにかのお役に立てれば幸いです。

## 作成したプログラム

WebSocketサーバが生成するランダムな数字をReactのクライアントが受信して、リアルタイムでグラフに表示するプログラムです。

![SD1.001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49564/253af40b-42ec-a2de-ca1d-cf35eee54334.png)

![sshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/49564/6029d3fb-21f3-eaa7-8abc-deb8c64925c9.png)


## 本ページで扱う内容
いずれも基本的な内容ですが、次を取り上げます。

- Socket.IOを使用したクライアントとサーバの通信
- ReactのHook処理
- RechartsのLineChartの描画

## 参考にしたサイト

1. ReactとSocket.IOのチュートリアル
https://www.valentinog.com/blog/socket-react/
TypeScriptのコーディングでは次のページが参考になりました。
https://qiita.com/okumurakengo/items/bb8f89912695e799e99e

1. Rechartsの紹介
https://qiita.com/quzq/items/8dc0ab885ab6a3c9cd77

1. React Hookの解説
https://qiita.com/keiya01/items/fc5c725fed1ec53c24c5
https://gotohayato.com/content/509/

1. useEffectにて値を更新するときの問題が解決したサイト
https://www.366service.com/jp/qa/ab82022d7ab06870a68911c84fbd5dc1

## 使用したバージョン
2020年10月時点の最新版を使用しています。

| Component | version |
| ---- | ---- |
| React | 16.13.1 |
| Recharts | 1.8.5 |
| socket.io-client | 2.3.1 |
| express | 4.17.1 |
| socket.io | 2.3.0 |
| npm | 6.14.8 |
| node | 14.10.1 |

開発環境をインストールしたOSは、Ubuntu 18.04.5 LTS　です。（MacBookのVS Codeからリモートで接続して開発しています。）

## サーバ側プログラム
Socket.IOのサーバに接続されたクライアントに対して、1000ミリ秒間隔でランダム値を送信します。
TypeScriptで記述しています。

### インストール
参考サイト１の通りに行いました。

```shell
$ mkdir socket-io-server && cd $_
$ npm init -y
$ npm i express socket.io
```

### 初期化
最初にExpressモジュールを使用して、HTTPサーバのインスタンスを生成します。

```ts
// App.js
import { randomInt } from "crypto";
import * as express from "express";
import * as http from "http";
import * as socketIo from "socket.io";

const port = process.env.PORT || 4001;
const app: express.Express = express();
const server: http.Server = http.createServer(app);
```
生成したインスタンスにSocket.IOのインスタンスを連結します。

```ts
const io: socketIo.Server = socketIo(server);
```
ここまでが初期化の部分です。

### クライアントからのメッセージ受信処理
このサーバ側のプログラムの唯一の要の部分です。
Socket.IOのonメソッドで、通信接続時のイベントハンドラを登録します。
第一パラメータでイベントの種類、第二パラメータでイベントハンドラを指定します。
"connection"は通信接続のイベントを表します。
登録したイベントハンドラは次のような処理を規定しています。

- 接続時に、すでにタイマーが設定されていたら、そのタイマーをクリアします。（重複のタイマーイベント発生を防止するため）
- Node.jsのタイマー関数setIntervalを使用して、1000ミリ秒間隔で接続クライアントにデータを送信するgetApiAndEmitを呼び出す。
- "disconnect"（通信切断）のイベントを受信したら、タイマーをクリアする、イベントハンドラを登録します。

```ts
io.on("connection", (socket: socketIo.Socket) => {
  let interval: NodeJS.Timeout;
  console.log("New client connected");
  if (interval) {
    clearInterval(interval);
  }
  interval = setInterval(() => getApiAndEmit(socket), 1000);
  socket.on("disconnect", () => {
    console.log("Client disconnected");
    clearInterval(interval);
  });
});

const getApiAndEmit = (socket: socketIo.Socket) => {
  const response: string = randomInt(100).toString();
  socket.emit("FromAPI", response);
};
```

上記のイベントハンドラについて、英語ですが、下記のSocket.IOの本家のページの解説を一読することは良いと思います。
https://socket.io/docs/

最後に、指定したポートでリッスンを開始します。

```ts
server.listen(port, () => console.log(`Listening on port ${port}`));
```

完成したコードはこちらです。

```ts
//app.ts
import { randomInt } from "crypto";
import * as express from "express";
import * as http from "http";
import * as socketIo from "socket.io";

const port = process.env.PORT || 4001;
const app: express.Express = express();

const server: http.Server = http.createServer(app);
const io: socketIo.Server = socketIo(server);

io.on("connection", (socket: socketIo.Socket) => {
  let interval: NodeJS.Timeout;
  console.log("New client connected");
  if (interval) {
    clearInterval(interval);
  }
  interval = setInterval(() => getApiAndEmit(socket), 1000);
  socket.on("disconnect", () => {
    console.log("Client disconnected");
    clearInterval(interval);
  });
});

const getApiAndEmit = (socket: socketIo.Socket) => {
  const response: string = randomInt(100).toString();
  socket.emit("FromAPI", response);
};

server.listen(port, () => console.log(`Listening on port ${port}`));
```

TypeScriptで記述したため、JavaScriptコードにトランスパイルします。
その後生成された、app.jsを実行します。

```shell
$ tsc app.ts
$ node app.js
```
実行後は、Listening on port 4001　というメッセージが表示されます。
次にクライアントプログラムを作成します。

## クライアント側プログラム
クライアント側プログラムは、サーバからのデータを受信して、グラフに反映します。

### インストール
最初にnpxを使用して、Reactのテンプレートを生成します。

```shell
$ npx create-react-app socket-io-client
```
生成したsocket-io-clientディレクトリに移ります。
クライアントでは、TypeScriptを使用したため、そのモジュールもインストールしています。

```shell
$ npm i typescript
$ npm i @types/socket.io @types/socket.io-client @types/express @types/node
$ npm i recharts
```
ESLintはcreate-react-appで設定されるデフォルトを使用しています。デフォルトでインストールされるモジュールについてはまだ良くわかっていないので、今後内容をまとめたいと思います。

### 初期化

Reactは関数コンポーネントで記述します。
サーバからのデータの受信を管理するために、２つのステートフルな変数を使用しています。
ステートフルな値の管理のために、フックAPIのuseStateとuseEffectを使用します。
chartUは後述するグラフ描画のコンポーネントです。

```js
import React, { useState, useEffect } from "react";
import socketIOClient from "socket.io-client";
 
import ChartU from "./chartU";
```

| ステートフルな変数名 | 用途 |
| ---- | ---- |
| dataarray | 受信データの配列 |
| count | 受信した回数 |

変数の宣言と初期化は次のとおりです。

```js
  const [dataarray, setDataarray] = useState([]);
  const [count, setCount] = useState(0);
```

### サーバからのデータ受信管理

ステートフルなデータの更新タイミングは、サーバからのSocket.IOの受信時に行います。
そのため、副作用フックのuseEffect()の中で、Socket.IOのイベントハンドラを規定しています。

```js
  useEffect(() => {
    const socket = socketIOClient(ENDPOINT);
    socket.on("FromAPI", data => {
      setDataarray(prevDataarray => {
        prevDataarray[prevDataarray.length] = data;
        return prevDataarray;
      });
      setCount(prevCount => prevCount + 1);
    });

    // CLEAN UP THE EFFECT
    return () => socket.disconnect();
    //
  }, []);
```
副作用のクリーンアップのために、socket.disconnect()を返しています。

#### 躓きポイント　更新用関数には、関数型の更新形式の引数が必要
配列 dataarrayに最新データを追加するために、useEffect()のイベントハンドラ内にステートフックで指定した更新用関数setDataarray()を実行しています。
下記のReactのドキュメントにあるように、更新用関数には関数型の更新形式が必要なため、上記のようにアロー関数で指定しています。
https://ja.reactjs.org/docs/hooks-reference.html#usestate

countの更新陽関数も、同様にアロー関数で指定しています。
このルールに気づかずに、解決までに時間がかかりました。

### グラフへ渡すデータ

RechartsのLineChartは別の関数コンポーネントで記述しています。
LineChartの引数に合わせるため、配列を連想配列に変換しています。

```js
      <ChartU value={dataarray.map((val, index) => {return {name: index , value: val};})}/>
```

### クライアント本体
クライアント本体のコードはこちらです。

```js
// App.js
import React, { useState, useEffect } from "react";
import socketIOClient from "socket.io-client";
 
import ChartU from "./chartU";
const ENDPOINT = "http://192.168.0.16:4001";

function App() {
  const [dataarray, setDataarray] = useState([]);
  const [count, setCount] = useState(0);

  useEffect(() => {
    const socket = socketIOClient(ENDPOINT);
    socket.on("FromAPI", data => {
      setDataarray(prevDataarray => {
        prevDataarray[prevDataarray.length] = data;
        return prevDataarray;
      });
      setCount(prevCount => prevCount + 1);
    });

    // CLEAN UP THE EFFECT
    return () => socket.disconnect();
    //
  }, []);

  return (
    <p>
      <div>{count}</div>
      <ChartU value={dataarray.map((val, index) => {return {name: index , value: val};})}/>
    </p>
  );
}

export default App;
```

### グラフ描画
クライアント本体から呼ばれる、グラフ描画コンポーネントです。
クライアント本体からvalueに渡されてくるデータをdata={props.value} でLineChartの属性に設定します。
その他の設定は、下記のRechartsのサンプルと同一です。
http://recharts.org/en-US/examples

グラフ描画のコードはこちらです。

```js
//chartU.js
import React from 'react';
import {
  LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
} from 'recharts';

export default function ChartU(props) {

  return (
    <LineChart
      width={800}
      height={400}
      data={props.value}
      margin={{
        top: 5, right: 30, left: 20, bottom: 5,
      }}
    >
      <CartesianGrid strokeDasharray="3 3" />
      <XAxis dataKey="name" />
      <YAxis />
      <Tooltip />
      <Legend />
      <Line type="monotone" dataKey="value" stroke="#8884d8" activeDot={{ r: 8 }} />
    </LineChart>
  );
}
```

## まとめ

少しフック機能で手間取りましたが、ReactでSocket.IOと通信してグラフ描画を実施できました。
グラフの更新ごとにX軸の範囲が変わるので、当初の意図とは異なりますがおもしろい動作になりました。

これからTypeScriptとReactを本格的に使いこなしたいと思います。

### 作成したソースコードの保管場所
拙いコードですが、GitHubに公開しました。
https://github.com/AkiraHojo/socket-io-server
https://github.com/AkiraHojo/socket-io-client