Skip to main content
Back to Blog
Tutorials
4 min read
November 30, 2024

How to Implement WebSocket Connections in Next.js

Add real-time WebSocket connections to your Next.js app using Socket.io for live data, notifications, and chat.

Ryel Banfield

Founder & Lead Developer

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.

WebSocketreal-timeNext.jsSocket.iotutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles