Reactでの使用方法
このガイドでは、React アプリケーション内で Socket.IO を使用する方法を説明します。
サンプル
構成
src
├── App.js
├── components
│   ├── ConnectionManager.js
│   ├── ConnectionState.js
│   ├── Events.js
│   └── MyForm.js
└── socket.js
Socket.IO クライアントは src/socket.js ファイルで初期化されます。
src/socket.js
import { io } from 'socket.io-client';
// "undefined" means the URL will be computed from the `window.location` object
const URL = process.env.NODE_ENV === 'production' ? undefined : 'https://:4000';
export const socket = io(URL);
デフォルトでは、Socket.IO クライアントはすぐにサーバーへの接続を開きます。autoConnect オプションを使用して、この動作を防ぐことができます。
export const socket = io(URL, {
  autoConnect: false
});
その場合、Socket.IO クライアントを接続させるために socket.connect() を呼び出す必要があります。これは、たとえば、ユーザーが接続する前に何らかの資格情報を提供する必要がある場合に役立ちます。
開発中は、サーバーで CORS を有効にする必要があります。
const io = new Server({
  cors: {
    origin: "https://:3000"
  }
});
io.listen(4000);
参照: CORS の処理
イベントリスナーは、App コンポーネントに登録されます。このコンポーネントは状態を保存し、props 経由で子コンポーネントに渡します。
参照: https://react.dokyumento.jp/learn/sharing-state-between-components
src/App.js
import React, { useState, useEffect } from 'react';
import { socket } from './socket';
import { ConnectionState } from './components/ConnectionState';
import { ConnectionManager } from './components/ConnectionManager';
import { Events } from "./components/Events";
import { MyForm } from './components/MyForm';
export default function App() {
  const [isConnected, setIsConnected] = useState(socket.connected);
  const [fooEvents, setFooEvents] = useState([]);
  useEffect(() => {
    function onConnect() {
      setIsConnected(true);
    }
    function onDisconnect() {
      setIsConnected(false);
    }
    function onFooEvent(value) {
      setFooEvents(previous => [...previous, value]);
    }
    socket.on('connect', onConnect);
    socket.on('disconnect', onDisconnect);
    socket.on('foo', onFooEvent);
    return () => {
      socket.off('connect', onConnect);
      socket.off('disconnect', onDisconnect);
      socket.off('foo', onFooEvent);
    };
  }, []);
  return (
    <div className="App">
      <ConnectionState isConnected={ isConnected } />
      <Events events={ fooEvents } />
      <ConnectionManager />
      <MyForm />
    </div>
  );
}
子コンポーネントは、次のように状態と socket オブジェクトを使用できます。
- src/components/ConnectionState.js
import React from 'react';
export function ConnectionState({ isConnected }) {
  return <p>State: { '' + isConnected }</p>;
}
- src/components/Events.js
import React from 'react';
export function Events({ events }) {
  return (
    <ul>
    {
      events.map((event, index) =>
        <li key={ index }>{ event }</li>
      )
    }
    </ul>
  );
}
- src/components/ConnectionManager.js
import React from 'react';
import { socket } from '../socket';
export function ConnectionManager() {
  function connect() {
    socket.connect();
  }
  function disconnect() {
    socket.disconnect();
  }
  return (
    <>
      <button onClick={ connect }>Connect</button>
      <button onClick={ disconnect }>Disconnect</button>
    </>
  );
}
- src/components/MyForm.js
import React, { useState } from 'react';
import { socket } from '../socket';
export function MyForm() {
  const [value, setValue] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  function onSubmit(event) {
    event.preventDefault();
    setIsLoading(true);
    socket.timeout(5000).emit('create-something', value, () => {
      setIsLoading(false);
    });
  }
  return (
    <form onSubmit={ onSubmit }>
      <input onChange={ e => setValue(e.target.value) } />
      <button type="submit" disabled={ isLoading }>Submit</button>
    </form>
  );
}
useEffect フックに関する注意事項
クリーンアップ
セットアップ関数に登録されたイベントリスナーは、イベントの重複登録を防ぐために、クリーンアップコールバックで削除する必要があります。
useEffect(() => {
  function onFooEvent(value) {
    // ...
  }
  socket.on('foo', onFooEvent);
  return () => {
    // BAD: missing event registration cleanup
  };
}, []);
また、イベントリスナーは名前付き関数であるため、socket.off() を呼び出すと、この特定のリスナーのみが削除されます。
useEffect(() => {
  socket.on('foo', (value) => {
    // ...
  });
  return () => {
    // BAD: this will remove all listeners for the 'foo' event, which may
    // include the ones registered in another component
    socket.off('foo');
  };
}, []);
依存関係
onFooEvent 関数は、次のように記述することもできます。
useEffect(() => {
  function onFooEvent(value) {
    setFooEvents(fooEvents.concat(value));
  }
  socket.on('foo', onFooEvent);
  return () => {
    socket.off('foo', onFooEvent);
  };
}, [fooEvents]);
これも機能しますが、その場合、onFooEvent リスナーは登録解除され、レンダリングごとに再登録されることに注意してください。
切断
コンポーネントのアンマウント時に Socket.IO クライアントを閉じる必要がある場合(たとえば、接続がアプリケーションの特定の部分でのみ必要な場合)、次のことを行う必要があります。
- セットアップフェーズで socket.connect()が呼び出されていることを確認します。
useEffect(() => {
  // no-op if the socket is already connected
  socket.connect();
  return () => {
    socket.disconnect();
  };
}, []);
厳密モードでは、開発中にバグを検出するためにすべての Effect が 2 回実行されるため、次のように表示されます。
- セットアップ: socket.connect()
- クリーンアップ: socket.disconnect()
- セットアップ: socket.connect()
- レンダリングごとに再接続を防ぐために、この Effect に依存関係を持たせないでください。
useEffect(() => {
  socket.connect();
  function onFooEvent(value) {
    setFooEvents(fooEvents.concat(value));
  }
  socket.on('foo', onFooEvent);
  return () => {
    socket.off('foo', onFooEvent);
    // BAD: the Socket.IO client will reconnect every time the fooEvents array
    // is updated
    socket.disconnect();
  };
}, [fooEvents]);
代わりに 2 つの Effect を使用できます。
import React, { useState, useEffect } from 'react';
import { socket } from './socket';
function App() {
  const [fooEvents, setFooEvents] = useState([]);
  useEffect(() => {
    // no-op if the socket is already connected
    socket.connect();
    return () => {
      socket.disconnect();
    };
  }, []);
  useEffect(() => {
    function onFooEvent(value) {
      setFooEvents(fooEvents.concat(value));
    }
    socket.on('foo', onFooEvent);
    return () => {
      socket.off('foo', onFooEvent);
    };
  }, [fooEvents]);
  // ...
}
重要な注意事項
これらの注意事項は、すべてのフロントエンドフレームワークに有効です。
ホットモジュールリローディング
Socket.IO クライアントの初期化を含むファイル(上記の例では src/socket.js ファイル)のホットリローディングを行うと、以前の Socket.IO 接続がアクティブなままになる可能性があります。これは、次のことを意味します.
- Socket.IO サーバーに複数の接続がある可能性があります。
- 以前の接続からイベントを受信する可能性があります。
唯一の既知の回避策は、この特定のファイルが更新されたときに**ページ全体をリロード**することです(または、ホットリローディングを完全に無効にすることもできますが、少し極端かもしれません)。
参照: https://webpack.dokyumento.jp/concepts/hot-module-replacement/
子コンポーネントのリスナー
子コンポーネントにイベントリスナーを登録することはお勧めしません。UI の状態がイベントの受信時間と結びついてしまうためです。コンポーネントがマウントされていない場合、メッセージが見落とされる可能性があります。
src/components/MyComponent.js
import React from 'react';
export default function MyComponent() {
  const [fooEvents, setFooEvents] = useState([]);
  useEffect(() => {
    function onFooEvent(value) {
      setFooEvents(previous => [...previous, value]);
    }
    // BAD: this ties the state of the UI with the time of reception of the
    // 'foo' events
    socket.on('foo', onFooEvent);
    return () => {
      socket.off('foo', onFooEvent);
    };
  }, []);
  // ...
}
一時的な切断
WebSocket 接続は非常に強力ですが、常に稼働しているとは限りません。
- ユーザーと Socket.IO サーバーの間にあるものはすべて、一時的な障害が発生したり、再起動されたりする可能性があります。
- サーバー自体が、自動スケーリングポリシーの一部として強制終了される場合があります。
- モバイルブラウザの場合、ユーザーが接続を失ったり、Wi-Fi から 4G に切り替えたりする可能性があります。
つまり、ユーザーに素晴らしいエクスペリエンスを提供するために、一時的な切断を適切に処理する必要があります。
良いニュースは、Socket.IO には役立つ機能がいくつか含まれていることです。以下を確認してください。