WebSockets enable real-time bidirectional communication. Here is how to add them to your Next.js application.
Step 1: Install Dependencies
pnpm add socket.io socket.io-client
Step 2: WebSocket Server
Create a custom server or use a standalone WebSocket server alongside Next.js.
// server/websocket.ts
import { Server as SocketIOServer } from "socket.io";
import type { Server as HTTPServer } from "http";
let io: SocketIOServer | null = null;
export function initializeWebSocket(httpServer: HTTPServer) {
if (io) return io;
io = new SocketIOServer(httpServer, {
cors: {
origin: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
methods: ["GET", "POST"],
},
pingTimeout: 60000,
pingInterval: 25000,
});
io.on("connection", (socket) => {
console.log(`Client connected: ${socket.id}`);
// Join a room
socket.on("join-room", (roomId: string) => {
socket.join(roomId);
socket.to(roomId).emit("user-joined", {
userId: socket.id,
timestamp: new Date().toISOString(),
});
});
// Leave a room
socket.on("leave-room", (roomId: string) => {
socket.leave(roomId);
socket.to(roomId).emit("user-left", { userId: socket.id });
});
// Broadcast message to room
socket.on("send-message", (data: { roomId: string; message: string; sender: string }) => {
io?.to(data.roomId).emit("new-message", {
id: crypto.randomUUID(),
message: data.message,
sender: data.sender,
timestamp: new Date().toISOString(),
});
});
// Typing indicator
socket.on("typing", (data: { roomId: string; user: string }) => {
socket.to(data.roomId).emit("user-typing", { user: data.user });
});
socket.on("disconnect", (reason) => {
console.log(`Client disconnected: ${socket.id} — ${reason}`);
});
});
return io;
}
export function getIO(): SocketIOServer {
if (!io) throw new Error("Socket.io not initialized");
return io;
}
Step 3: Client Hook
// hooks/useSocket.ts
"use client";
import { useEffect, useRef, useState, useCallback } from "react";
import { io, Socket } from "socket.io-client";
interface UseSocketOptions {
url?: string;
autoConnect?: boolean;
}
export function useSocket({ url, autoConnect = true }: UseSocketOptions = {}) {
const socketRef = useRef<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [transport, setTransport] = useState<string>("N/A");
useEffect(() => {
if (!autoConnect) return;
const socketUrl = url || process.env.NEXT_PUBLIC_WS_URL || "";
const socket = io(socketUrl, {
transports: ["websocket", "polling"],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
});
socketRef.current = socket;
socket.on("connect", () => {
setIsConnected(true);
setTransport(socket.io.engine.transport.name);
});
socket.io.engine.on("upgrade", (transport) => {
setTransport(transport.name);
});
socket.on("disconnect", () => {
setIsConnected(false);
setTransport("N/A");
});
return () => {
socket.disconnect();
};
}, [url, autoConnect]);
const emit = useCallback((event: string, data?: unknown) => {
socketRef.current?.emit(event, data);
}, []);
const on = useCallback((event: string, handler: (...args: unknown[]) => void) => {
socketRef.current?.on(event, handler);
return () => {
socketRef.current?.off(event, handler);
};
}, []);
return {
socket: socketRef.current,
isConnected,
transport,
emit,
on,
};
}
Step 4: Typed Socket Events
// types/socket.ts
export interface ServerToClientEvents {
"new-message": (data: {
id: string;
message: string;
sender: string;
timestamp: string;
}) => void;
"user-joined": (data: { userId: string; timestamp: string }) => void;
"user-left": (data: { userId: string }) => void;
"user-typing": (data: { user: string }) => void;
"presence-update": (data: { online: string[] }) => void;
}
export interface ClientToServerEvents {
"join-room": (roomId: string) => void;
"leave-room": (roomId: string) => void;
"send-message": (data: { roomId: string; message: string; sender: string }) => void;
"typing": (data: { roomId: string; user: string }) => void;
}
Step 5: Live Chat Component
"use client";
import { useState, useEffect, useRef } from "react";
import { useSocket } from "@/hooks/useSocket";
interface Message {
id: string;
message: string;
sender: string;
timestamp: string;
}
export function LiveChat({ roomId, username }: { roomId: string; username: string }) {
const { isConnected, emit, on } = useSocket();
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isConnected) return;
emit("join-room", roomId);
const offMessage = on("new-message", (data) => {
setMessages((prev) => [...prev, data as Message]);
});
const offTyping = on("user-typing", (data) => {
const { user } = data as { user: string };
setTypingUsers((prev) => [...new Set([...prev, user])]);
setTimeout(() => {
setTypingUsers((prev) => prev.filter((u) => u !== user));
}, 3000);
});
return () => {
emit("leave-room", roomId);
offMessage();
offTyping();
};
}, [isConnected, roomId, emit, on]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
function handleSend(e: React.FormEvent) {
e.preventDefault();
if (!input.trim()) return;
emit("send-message", { roomId, message: input, sender: username });
setInput("");
}
return (
<div className="flex h-[500px] flex-col rounded-2xl border dark:border-gray-700">
{/* Connection status */}
<div className="flex items-center gap-2 border-b p-3 dark:border-gray-700">
<span
className={`h-2 w-2 rounded-full ${
isConnected ? "bg-green-500" : "bg-red-500"
}`}
/>
<span className="text-sm text-gray-500">
{isConnected ? "Connected" : "Disconnected"}
</span>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.sender === username ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[70%] rounded-2xl px-4 py-2 ${
msg.sender === username
? "bg-blue-600 text-white"
: "bg-gray-100 dark:bg-gray-800"
}`}
>
<p className="text-xs font-medium opacity-70">{msg.sender}</p>
<p className="text-sm">{msg.message}</p>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* Typing indicator */}
{typingUsers.length > 0 && (
<p className="px-4 text-xs text-gray-400">
{typingUsers.join(", ")} {typingUsers.length === 1 ? "is" : "are"} typing...
</p>
)}
{/* Input */}
<form onSubmit={handleSend} className="flex gap-2 border-t p-3 dark:border-gray-700">
<input
type="text"
value={input}
onChange={(e) => {
setInput(e.target.value);
emit("typing", { roomId, user: username });
}}
placeholder="Type a message..."
className="flex-1 rounded-lg border px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-800"
/>
<button
type="submit"
disabled={!isConnected || !input.trim()}
className="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
>
Send
</button>
</form>
</div>
);
}
Step 6: Connection Recovery
// hooks/useSocketWithRecovery.ts
"use client";
import { useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";
export function useSocketWithRecovery() {
const socketRef = useRef<Socket | null>(null);
const [status, setStatus] = useState<"connecting" | "connected" | "disconnected" | "error">(
"connecting"
);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
const socket = io({
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000,
reconnectionDelayMax: 10000,
});
socketRef.current = socket;
socket.on("connect", () => {
setStatus("connected");
setRetryCount(0);
});
socket.on("disconnect", () => setStatus("disconnected"));
socket.on("connect_error", () => {
setStatus("error");
setRetryCount((prev) => prev + 1);
});
socket.io.on("reconnect_attempt", (attempt) => {
setRetryCount(attempt);
});
socket.io.on("reconnect", () => {
setStatus("connected");
setRetryCount(0);
});
return () => {
socket.disconnect();
};
}, []);
return { socket: socketRef.current, status, retryCount };
}
Summary
- Use Socket.io for WebSocket connections with fallback to polling
- Type your events for end-to-end safety
- Handle reconnection gracefully with status indicators
- Room-based architecture scales to multiple channels
Need Real-Time Features?
We build real-time collaborative applications with WebSocket technology. Contact us to discuss your project.