Reactの勉強をはじめたので、Socket.IOと通信するコードを参考に、取得したデータをRechartsで表示しました。
自分の理解用に作成しましたが、なにかのお役に立てれば幸いです。
作成したプログラム
WebSocketサーバが生成するランダムな数字をReactのクライアントが受信して、リアルタイムでグラフに表示するプログラムです。
本ページで扱う内容
いずれも基本的な内容ですが、次を取り上げます。
- Socket.IOを使用したクライアントとサーバの通信
- ReactのHook処理
- RechartsのLineChartの描画
参考にしたサイト
-
ReactとSocket.IOのチュートリアル
https://www.valentinog.com/blog/socket-react/
TypeScriptのコーディングでは次のページが参考になりました。
https://qiita.com/okumurakengo/items/bb8f89912695e799e99e -
Rechartsの紹介
https://qiita.com/quzq/items/8dc0ab885ab6a3c9cd77 -
React Hookの解説
https://qiita.com/keiya01/items/fc5c725fed1ec53c24c5
https://gotohayato.com/content/509/ -
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で記述しています。
インストール
参考サイト1の通りに行いました。
$ mkdir socket-io-server && cd $_
$ npm init -y
$ npm i express socket.io
初期化
最初にExpressモジュールを使用して、HTTPサーバのインスタンスを生成します。
// 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のインスタンスを連結します。
const io: socketIo.Server = socketIo(server);
ここまでが初期化の部分です。
クライアントからのメッセージ受信処理
このサーバ側のプログラムの唯一の要の部分です。
Socket.IOのonメソッドで、通信接続時のイベントハンドラを登録します。
第一パラメータでイベントの種類、第二パラメータでイベントハンドラを指定します。
"connection"は通信接続のイベントを表します。
登録したイベントハンドラは次のような処理を規定しています。
- 接続時に、すでにタイマーが設定されていたら、そのタイマーをクリアします。(重複のタイマーイベント発生を防止するため)
- Node.jsのタイマー関数setIntervalを使用して、1000ミリ秒間隔で接続クライアントにデータを送信するgetApiAndEmitを呼び出す。
- "disconnect"(通信切断)のイベントを受信したら、タイマーをクリアする、イベントハンドラを登録します。
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/
最後に、指定したポートでリッスンを開始します。
server.listen(port, () => console.log(`Listening on port ${port}`));
完成したコードはこちらです。
//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を実行します。
$ tsc app.ts
$ node app.js
実行後は、Listening on port 4001 というメッセージが表示されます。
次にクライアントプログラムを作成します。
クライアント側プログラム
クライアント側プログラムは、サーバからのデータを受信して、グラフに反映します。
インストール
最初にnpxを使用して、Reactのテンプレートを生成します。
$ npx create-react-app socket-io-client
生成したsocket-io-clientディレクトリに移ります。
クライアントでは、TypeScriptを使用したため、そのモジュールもインストールしています。
$ npm i typescript
$ npm i @types/socket.io @types/socket.io-client @types/express @types/node
$ npm i recharts
ESLintはcreate-react-appで設定されるデフォルトを使用しています。デフォルトでインストールされるモジュールについてはまだ良くわかっていないので、今後内容をまとめたいと思います。
初期化
Reactは関数コンポーネントで記述します。
サーバからのデータの受信を管理するために、2つのステートフルな変数を使用しています。
ステートフルな値の管理のために、フックAPIのuseStateとuseEffectを使用します。
chartUは後述するグラフ描画のコンポーネントです。
import React, { useState, useEffect } from "react";
import socketIOClient from "socket.io-client";
import ChartU from "./chartU";
ステートフルな変数名 | 用途 |
---|---|
dataarray | 受信データの配列 |
count | 受信した回数 |
変数の宣言と初期化は次のとおりです。
const [dataarray, setDataarray] = useState([]);
const [count, setCount] = useState(0);
サーバからのデータ受信管理
ステートフルなデータの更新タイミングは、サーバからのSocket.IOの受信時に行います。
そのため、副作用フックのuseEffect()の中で、Socket.IOのイベントハンドラを規定しています。
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の引数に合わせるため、配列を連想配列に変換しています。
<ChartU value={dataarray.map((val, index) => {return {name: index , value: val};})}/>
クライアント本体
クライアント本体のコードはこちらです。
// 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
グラフ描画のコードはこちらです。
//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