Next.js での使用法
このガイドでは、Next.js アプリケーション内で Socket.IO を使用する方法を説明します。
サーバー
Socket.IO サーバーは、Next.js と同じ基盤となる HTTP サーバーを共有できます。プロジェクトのルートに server.js ファイルを作成するだけです。
import { createServer } from "node:http";
import next from "next";
import { Server } from "socket.io";
const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 3000;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handler = app.getRequestHandler();
app.prepare().then(() => {
  const httpServer = createServer(handler);
  const io = new Server(httpServer);
  io.on("connection", (socket) => {
    // ...
  });
  httpServer
    .once("error", (err) => {
      console.error(err);
      process.exit(1);
    })
    .listen(port, () => {
      console.log(`> Ready on http://${hostname}:${port}`);
    });
});
server.js ファイルはアプリケーションのエントリポイントになります。
{
  "scripts": {
-   "dev": "next dev",
+   "dev": "node server.js",
    "build": "next build",
-   "start": "next start",
+   "start": "NODE_ENV=production node server.js",
    "lint": "next lint"
  }
}
これで完了です!
参考: https://nextjs.dokyumento.jp/docs/pages/building-your-application/configuring/custom-server
これは、App Router と Pages Router の両方で動作します。
クライアント
クライアント側では、React ガイド のすべてのヒントが有効です。
唯一の違いは、サーバーサイドレンダリング (SSR) から Socket.IO クライアントを除外する必要があることです。
- App Router
- Pages Router
構造
├── src
│ ├── app
│ │ └── page.js
│ └── socket.js
└── package.json
"use client";
import { io } from "socket.io-client";
export const socket = io();
"use client" は、ファイルがクライアントバンドルの一部であり、サーバーレンダリングされないことを示します。
参考: https://nextjs.dokyumento.jp/docs/app/building-your-application/rendering/client-components
"use client";
import { useEffect, useState } from "react";
import { socket } from "../socket";
export default function Home() {
  const [isConnected, setIsConnected] = useState(false);
  const [transport, setTransport] = useState("N/A");
  useEffect(() => {
    if (socket.connected) {
      onConnect();
    }
    function onConnect() {
      setIsConnected(true);
      setTransport(socket.io.engine.transport.name);
      socket.io.engine.on("upgrade", (transport) => {
        setTransport(transport.name);
      });
    }
    function onDisconnect() {
      setIsConnected(false);
      setTransport("N/A");
    }
    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);
    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
    };
  }, []);
  return (
    <div>
      <p>Status: { isConnected ? "connected" : "disconnected" }</p>
      <p>Transport: { transport }</p>
    </div>
  );
}
構造
├── src
│ ├── pages
│ │ └── index.js
│ └── socket.js
└── package.json
import { io } from "socket.io-client";
const isBrowser = typeof window !== "undefined";
export const socket = isBrowser ? io() : {};
isBrowser チェックは重要です。Next.js がサーバーサイドレンダリング時に Socket.IO クライアントを作成しようとしないようにするためです。
参考: https://nextjs.dokyumento.jp/docs/pages/building-your-application/rendering/client-side-rendering
import { useEffect, useState } from "react";
import { socket } from "../socket";
export default function Home() {
  const [isConnected, setIsConnected] = useState(false);
  const [transport, setTransport] = useState("N/A");
  useEffect(() => {
    if (socket.connected) {
      onConnect();
    }
    function onConnect() {
      setIsConnected(true);
      setTransport(socket.io.engine.transport.name);
      socket.io.engine.on("upgrade", (transport) => {
        setTransport(transport.name);
      });
    }
    function onDisconnect() {
      setIsConnected(false);
      setTransport("N/A");
    }
    socket.on("connect", onConnect);
    socket.on("disconnect", onDisconnect);
    return () => {
      socket.off("connect", onConnect);
      socket.off("disconnect", onDisconnect);
    };
  }, []);
  return (
    <div>
      <p>Status: { isConnected ? "connected" : "disconnected" }</p>
      <p>Transport: { transport }</p>
    </div>
  );
}
次のように使用することもできます。
const [isConnected, setIsConnected] = useState(socket.connected);
ではなく
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
  if (socket.connected) {
    onConnect();
  }
  // ...
});
しかし、これはクライアントレンダリングされたページがサーバーレンダリングされた出力と一致しない可能性があるため、Next.js コンパイラからいくつかの警告が発生します。
キャッチされないエラー: テキストコンテンツがサーバーレンダリングされた HTML と一致しません。
上記の例では、transport 変数は Socket.IO 接続を確立するために使用される低レベルのトランスポートであり、次のいずれかになります。
- HTTP ロングポーリング ("polling")
- WebSocket ("websocket")
- WebTransport ("webtransport")
すべてがうまくいけば、次のように表示されるはずです。
Status: connected
Transport: websocket
その後、Socket.IO サーバーとクライアント間でメッセージを交換できます。
- メッセージを送信するには socket.emit()を使用します。
socket.emit("hello", "world");
- メッセージを受信するには socket.on()を使用します。
socket.on("hello", (value) => {
  // ...
});
これで全部です。読んでいただきありがとうございます!