14
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-10-07

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

作成したプログラム

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

SD1.001.png

sshot.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

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

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

  4. 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

14
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?