1. ホーム
  2. reactjs

[解決済み] Next.jsでWebSocketを利用する

2022-02-07 16:12:44

質問

Next.jsのページを使用してWebSocketサーバーに接続するための最良の方法は何でしょうか?私は、ユーザーが1つの接続でページ間を移動でき、ページを閉じるときにWebSockets接続も閉じられるようにしたいです。私はReactのContext APIを使用しようとしました。

const WSContext = createContext(null);

const Wrapper = ({ children }) => {
  const instance = WebSocket("ws://localhost:3000/ws");

  return <WSContext.Provider value={instance}>{children}</WSContext.Provider>;
};

export const useWS = () => useContext(WSContext);

export default Wrapper;

これは素晴らしい働きですが、接続を作成するときにはそうではありません。基本的な new WebSocket の構文が使えないので、サードパーティのライブラリ、例えば react-use-websocket というのが嫌なんです。さらに気になるのは、接続を閉じることができないことです。Contextはページが閉じられたことを知らないだけで、ライブラリは接続を閉じるためのフックを提供していない。

Next.jsでWebSocketの接続を処理する場合、どのような方法があるのか教えてほしいです。

どのように解決するのですか?

Next.jsでwsを動作させるためには、複数の事柄を行う必要があります。

まず、wsのコードをどこで実行させたいかを認識することが重要です。Next.js上のReactのコードは、2つの環境で動作します。サーバー上(ページを構築するときやssrを使用するとき)とクライアント上です。

ページ構築時にws接続を行うことはほとんど意味がないので、ここではクライアントサイドのwsのみを取り上げます。

グローバルなWebsocketクラスは、ブラウザのみの機能であり、サーバには存在しない。そのため、ブラウザでコードが実行されるまで、インスタンス化を防止する必要があります。そのための簡単な方法の1つは、次のようなものです。

export const isBrowser = typeof window !== "undefined";

export const wsInstance = isBrowser ? new Websocket(...) : null;

また、インスタンスを保持するためにreact contextを使用する必要はなく、グローバルスコープに保持してインスタンスをインポートすることは、接続を遅延して開きたい場合を除き、完全に可能である。

それでもリアクトコンテキストを使う(あるいはリアクトツリーのどこかでwsクライアントを初期化する)場合は、次のことが重要です。 メモする のインスタンスは、リアクトノードの更新のたびに生成されないようにします。

const wsInstance = useMemo(() => isBrowser ? new Websocket(...) : null, []);

または

const [wsInstance] = useState(() => isBrowser ? new Websocket(...) : null);

reactで作成されたイベントハンドラ登録はすべて useEffect これはメモリリークを防ぐためです。また、依存関係の配列も指定する必要があります。コンポーネントがアンマウントされた場合は useEffect フックは、イベントリスナーも削除します。

もし、ws を再定義して、現在の接続を破棄したい場合は、以下のような方法が可能です。

const [wsInstance, setWsInstance] = useState(null);

// Call when updating the ws connection
const updateWs = useCallback((url) => {
   if(!browser) return setWsInstance(null);
   
   // Close the old connection
   if(wsInstance?.readyState !== 3)
     wsInstance.close(...);

   // Create a new connection
   const newWs = new WebSocket(url);
   setWsInstance(newWs);
}, [wsInstance])


// (Optional) Open a connection on mount
useEffect(() => {
 if(isBrowser) { 
   const ws = new WebSocket(...);
   setWsInstance(ws);
 }

 return () => {
  // Cleanup on unmount if ws wasn't closed already
  if(ws?.readyState !== 3) 
   ws.close(...)
 }
}, [])