Skip to content

☠️ ☠️ DO NOT FOLLOW THIS ☠️ ☠️git

We are keeping this page just for archive, do not follow this patter if you can avoid it. It makes the next app slow, harder to debug and break HMR

Sockets in Next.js

Some activations requires multiple browsers to be synced, the following is a step by step on how to get your sockets in Next.js running.

INFO

The following pattern ensures there is only one connection per browser tab, and that the connection is reused across components.

Setting up the server

Install the required dependencies

bash
npm i socket.io socket.io-client ts-node

Create a new file under ./src/server/index.ts and paste the following code. This file will be used to create a socket server that listens for incoming connections and emits events to connected clients. It will replace the default Next.js server, wrapping it in our own server that has sockets.

typescript
import next from "next";
import { createServer } from "http";
import { Server } from "socket.io";
import { SOCKET_EVENTS } from "../lib/const";

const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const PORT = Number(process.env.NEXT_PUBLIC_PORT || process.env.PORT) || 3000;
  const server = createServer(async (req, res) => {
    try {
      return await handle(req, res);
    } catch (err) {
      console.error("Error occurred handling", req.url, err);
      res.statusCode = 500;
      res.end("internal server error");
    }
  })
    .once("error", (err) => {
      console.error(err);
      process.exit(1);
    })
    .listen(PORT, () => {
      if (process.send) process.send("ready");
      console.log(`\x1b[36m%s\x1b[0m`, `> Ready on http://localhost:${PORT}`);
    });

  const io = new Server(server);

  io.on("connection", (socket) => {
    socket.on(SOCKET_EVENTS.JOIN_ROOM, (room: string) => {
      console.log("Joining room: ", room);
      socket.join(room);
    });
    socket.on(SOCKET_EVENTS.LEAVE_ROOM, (room: string) => {
      console.log("leaving room: ", room);
      socket.leave(room);
    });

    // start adding your custom events `NAVIGATE_TO` is just an example
    socket.on(
      SOCKET_EVENTS.NAVIGATE_TO,
      ({ room, path }: { room: string; path: string }) => {
        socket.to(room).emit(SOCKET_EVENTS.NAVIGATE_TO, path);
      }
    );
  });
});

Update your package.json scripts to include the following:

json
"dev": "TURBOPACK=1 NODE_ENV=development ts-node --project tsconfig.server.json src/server/index.ts",
"build": "rm -rf dist && tsc --project tsconfig.server.json && next build",
"start": "NODE_ENV=production NEXT_PUBLIC_PORT=$npm_config_port node dist/server/index.js",

Create a ./tsconfig.server.json file to compile the new server and add the following content

json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "dist",
    "target": "es2017",
    "isolatedModules": false,
    "noEmit": false
  },
  "include": ["src/server/**/*.ts"]
}

Important

Change the moduleResolution to node in the main ./tsconfig.json file.

Remember

Adjust your PM2 configuration in ecosystem.config.js to correctly point to the built server file.

See more: Deploying to NUCs

Socket Components and hooks for react.

You can structure your folders as you prefer. Below is an example setup for socket-related components and hooks. If possible use the plop generator to create the files.

  1. Socket Initialization: Create a file at ./src/components/SocketConnect/socket.ts:
typescript
"use client";
import { io } from "socket.io-client";
export const socket = io({
  reconnection: true,
  reconnectionDelay: 500,
  reconnectionAttempts: 10,
});
  1. Socket Context and Hooks: Create a file at ./src/components/SocketConnect/SocketConnect.ts:
typescript
"use client";
import { SOCKET_EVENTS } from "@/lib/const";
import { useParams } from "next/navigation";
import {
  ReactNode,
  createContext,
  useContext,
  useEffect,
  useState,
} from "react";
import { Socket } from "socket.io-client";
import { socket as newSocket } from "./socket";

// Create a context with a default value of null for the socket
const SocketContext = createContext<Socket | null>(null);

export const useSocket = () => useContext(SocketContext);

/**
 * A custom hook that allows components to listen for socket events and execute a callback function when the event occurs.
 * @param eventName - The name of the socket event to listen for.
 * @param callback - The function to be executed when the socket event occurs. It takes one argument, `arg1`, which represents the data received from the event.
 */
export function useSocketOn<Arg1>(
  eventName: keyof typeof SOCKET_EVENTS,
  callback: (arg1: Arg1) => void
) {
  const socket = useSocket();

  useEffect(() => {
    if (socket) {
      socket.on(eventName, callback);
    }

    return () => {
      if (socket) {
        socket.off(eventName, callback);
      }
    };
  }, [socket, eventName, callback]);
}

export const useSocketEmit = () => {
  const socket = useSocket();
  const cb = (eventName: keyof typeof SOCKET_EVENTS, data: any) => {
    ``;
    if (socket) {
      socket.emit(eventName, data);
    }
  };
  return cb;
};

export const SocketProvider = ({ children }: { children: ReactNode }) => {
  const { room } = useParams();
  const [connected, setConnected] = useState(false);
  const [socket, setSocket] = useState<Socket | null>(newSocket);

  useEffect(() => {
    const onConnect = () => {
      if (room) {
        newSocket.emit(SOCKET_EVENTS.JOIN_ROOM, room);
      } else {
        console.warn("No room provided. Cannot join room.");
      }
      setConnected(true);
    };
    const onDisconnect = () => {
      console.log("disconnecting socket");
    };

    if (newSocket.connected) {
      onConnect();
    }

    newSocket.on("connect", onConnect);
    newSocket.on("disconnect", onDisconnect);

    if (!connected) {
      newSocket.connect();
    }

    return () => {
      newSocket.off("connect", onConnect);
      newSocket.off("disconnect", onDisconnect);
    };
  }, [connected, room]);

  return (
    <SocketContext.Provider value={socket}>{children}</SocketContext.Provider>
  );
};

Implementing the SocketProvider

Wrap your template.tsx with the SocketProvider component

tsx
import SocketProvider from "@/components/atoms/SocketConnect";

export default function Template({ children }: { children: React.ReactNode }) {
  return (
    <SocketProvider>
      <main>{children}</main>
    </SocketProvider>
  );
}

And now from within your components, you can use the useSocketOn and useSocketEmit hooks to listen for and emit socket events.

tsx
import {
  useSocketOn,
  useSocketEmit,
} from "@/components/SocketConnect/SocketConnect";
import { SOCKET_EVENTS } from "@/lib/const";

export default function MyComponent() {
  const emit = useSocketEmit();
  const onEvent = (data: any) => {
    console.log("Received data: ", data);
  };

  useSocketOn(SOCKET_EVENTS.MY_EVENT, onEvent);

  return (
    <button onClick={() => emit(SOCKET_EVENTS.MY_EVENT, { data: "Hello" })}>
      Emit Event
    </button>
  );
}

This setup provides a robust foundation for managing real-time communication in your Next.js applications using sockets.