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 as a separate Express app working
INFO
The following pattern ensures there is only one connection per browser tab, and that the connection is reused across components.
Install some dependencies
npm i express socket.io socket.io-client
npm i --save-dev @types/express
You may want to load some env vars in your app, so you can use the dotenv
package to load them from a .env
file.
npm i dotenv
Setting up the server
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.
import * as dotenv from "dotenv";
import * as express from "express";
import { createServer } from "node:http";
import { Server, Socket } from "socket.io";
import { SOCKET_EVENTS } from "../components/SocketConnect/socketConst";
dotenv.config();
const app = express();
const server = createServer(app);
const io = new Server(server, {
cors: {
origin: "*",
},
});
const port = 3010;
app.get("/", (req, res) => {
res.send("Hello, world!");
});
const attachEvent = (
event: (typeof SOCKET_EVENTS)[keyof typeof SOCKET_EVENTS],
socket: Socket
) => {
socket.on(event, ({ room, data }: { room: string; data: any }) => {
console.log("Socket Event:", event, room, data);
socket.to(room).emit(event, data);
});
};
io.on("connection", (socket) => {
console.log("a user connected");
socket.on("disconnect", () => {
console.log("user disconnected");
});
socket.on(SOCKET_EVENTS.JOIN_ROOM, (room) => {
console.log("Joinig room:", room);
socket.join(room);
});
socket.on(SOCKET_EVENTS.LEAVE_ROOM, (room) => {
console.log("Leaving room:", room);
socket.leave(room);
});
attachEvent(SOCKET_EVENTS.REFRESH, socket);
// your other custom events
});
server.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Lets add some quick scripts to the package json
"server:build": "tsc ./src/server/index.ts --outDir ./.server",
"server:start": "npm run server:build && node ./.server/server/index.js"
Create a ./tsconfig.server.json
file to compile the new server and add the following content
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "dist",
"target": "es2017",
"isolatedModules": false,
"noEmit": false
},
"include": ["src/server/**/*.ts"]
}
Add the server files to the ignore list of the main ts config file tsconfig.json
so it looks as follows
{
"exclude": ["node_modules", "src/server"]
}
Add the pm2 app to the config
module.exports = {
apps: [
// other apps
{
name: "node-helper-server",
script: "./.server/server/index.js",
log_date_format: "DD-MM HH:mm:ss.SSS",
},
],
};
Add the built folder to the .bitignore
# node server
.server
Next.js / React integration
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.
src
├── components
│ └── SocketConnect
│ ├── socket.ts
│ ├── socketConst.ts
│ └── useSocket.ts
socket.ts
Socket Initialization: Create a file at./src/components/SocketConnect/socket.ts
TODO this is probably better as a factory function.
"use client";
import { io } from "socket.io-client";
export const socket = process.env.NEXT_PUBLIC_SOCKET_URL
? io(process.env.NEXT_PUBLIC_SOCKET_URL, {
reconnection: true,
reconnectionDelay: 500,
})
: null;
socketConst.ts
Socket Events: Create a file at./src/components/SocketConnect/socketConst.ts
:
export const SOCKET_EVENTS = {
JOIN_ROOM: "JOIN_ROOM",
LEAVE_ROOM: "LEAVE_ROOM",
REFRESH: "REFRESH",
NAVIGATE_TO: "NAVIGATE_TO",
// add your custom events here
} as const;
SocketConnect.ts
Socket Context and Hooks: Create a file at./src/components/SocketConnect/SocketConnect.ts
:
"use client";
import {
ReactNode,
createContext,
useContext,
useEffect,
useState,
} from "react";
import { Socket } from "socket.io-client";
import { socket as newSocket } from "./socket";
import { SOCKET_EVENTS } from "./socketConst";
// 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,
room,
}: {
children: ReactNode;
room: string;
}) => {
// const { room } = useParams();
const [connected, setConnected] = useState(false);
const [socket] = 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
import SocketProvider from "@/components/atoms/SocketConnect";
export default function Template({ children }: { children: React.ReactNode }) {
return (
<SocketProvider room={"some-room"}>
<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.
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>
);
}