reactjsnode.jsnext.jswebsocketsocket.io

How to prevent socket-io instantiating two sockets in React / Next.js (currently instantiates on server *and* client)


I'm trying to get a basic WebSocket setup to work:

Here's the most important files:

Server

My Express server only has a single file that looks like this:

index.ts

import * as socket from "socket.io";
import express from "express";
import http from "http";
import cors from "cors";

const app = express();
app.use(cors());

const server = http.createServer(app);

const io = new socket.Server(server, {
  cors: {
    origin: true,
  },
});

io.on("connection", async (socket) => {
  console.log(`Connection established`, socket.id);

  socket.on("disconnect", () => {
    console.log("user disconnected", socket.id);
  });

  socket.on("chat-message", (text) => {
    console.log(text);
    socket.broadcast.emit("chat-message", text);
  });
});

server.listen(4000, () => {
  console.log("Server running on http://localhost:4000");
});

Client

On the client, I have two relevant files:

app/lib/socket.ts

Here's the problem: This code gets executed twice when the app is first started! I see the initialize socket log on the server (i.e. in my Terminal where I run the Next.js app) and on the client (i.e. in the Browser console).

import { io } from 'socket.io-client';

const url = 'http://localhost:4000';
console.log(`initialize socket`); 
export const socket = io(url);

app/page.tsx

"use client";

import { useState, useEffect } from "react";
import { socket } from "./lib/socket";

interface Message {
  text: string;
}

export default function Home() {
  const [message, setMessage] = useState("");
  const [messageHistory, setMessageHistory] = useState<Message[]>([
    {
      text: "hello",
    },
    {
      text: "world",
    },
  ]);

  const newMessageReceived = (e) => {
    console.log(`received message: `, e);
    setMessageHistory((oldMessageHistory) => [...oldMessageHistory, { text: e }]);
  };

  useEffect(() => {
    socket.on("chat-message", newMessageReceived);

    return () => {
      console.log(`return from useEffect`);
      socket.off("chat-message", newMessageReceived);
    };
  }, []);

  const sendMessage = async (e: any) => {
    e.preventDefault();
    const newMessage = message;
    setMessage("");
    console.log(`sendMessage`, newMessage);
    socket.emit("chat-message", newMessage);
    setMessageHistory((oldMessageHistory) => [...oldMessageHistory, { text: newMessage }]);
  };

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
        <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl lg:static lg:w-auto  lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
          <code className="font-mono font-bold">CoolChat 😎</code>
        </p>
      </div>
      <div className="">
        {messageHistory.map((message, i) => {
          return <div key={i}>{message.text}</div>;
        })}
      </div>

      <form
        id="text-input-container"
        className="bg-gray-300 py-4 px-2 w-full flex items-center justify-center"
        onSubmit={sendMessage}
      >
        <div className="text-center bg-white w-full md:w-1/3 px-3 py-2 flex gap-3 rounded-xl drop-shadow-2xl">
          <input
            name="message"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            id="message"
            className="focus:outline-none px-2 flex-1 rounded-xl"
            type="text"
            placeholder="What do you want to say?"
          />
          <button type="submit" className="rounded-xl px-3 py-2 bg-gray-600 text-gray-100 text-sm">
            Send
          </button>
        </div>
      </form>
    </main>
  );
}

Here's what happens when I run the code:

  1. start server with npm run dev
  2. server prints Server running on http://localhost:4000
  3. start client with npm run dev
  4. open new browser window and go to localhost:3000
  5. client (terminal) prints: initialize socket
  6. client (browser) prints: initialize socket
  7. server prints Connection established wQGEI_1USi_4T9n-AAAB
  8. server prints Connection established 1SPz0765uUEvMQgBAAAD
  9. (sometimes a third time): server prints Connection established BOAeHMUMeJ5zb2EJAAAF
  10. open a second browser window and go to localhost:3000
  11. client (browser) prints: initialize socket
  12. server prints Connection established um8qdP-yV9btkqVAAAAH
  13. open a third browser window and go to localhost:3000
  14. client (browser) prints: initialize socket
  15. server prints Connection established um8qdP-yV9btkqVAAAAH

So, as you see, the first time I connect to localhost:3000, the socket gets initialized twice — once the server and once the client.

All subsequent times, with new browser windows navigating to localhost:3000, the socket only initializes once (in the browser, not at all on the server) as I would expect.


Solution

  • As you mentioned you are using Next.js 14 and facing this issue in your local environment. It's likely that React is rendering your component twice which leads to multiple connections being established.

    I would recommend you to disable reactStrictMode in the next.config.js file. It should fix the issue for you.

    // next.config.js
    module.exports = {
      reactStrictMode: false,
    }