Building Real-Time Features in Next.js with WebSocket

June 20, 2024 (11mo ago)

Building Real-Time Features in Next.js with WebSocket

Learn how to implement real-time features in your Next.js application using WebSocket, including live chat, notifications, and collaborative editing.

In this article:

Understanding Real-Time Communication

Real-time features are essential for modern web applications:

Setting Up WebSocket Server

First, install the required dependencies:

npm install socket.io socket.io-client

Create a WebSocket server:

// server/socket.ts
import { Server } from 'socket.io'
 
import next from 'next'
 
import { createServer } from 'http'
import { parse } from 'url'
 
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
 
app.prepare().then(() => {
  const server = createServer((req, res) => {
    const parsedUrl = parse(req.url!, true)
    handle(req, res, parsedUrl)
  })
 
  const io = new Server(server, {
    cors: {
      origin: process.env.NEXT_PUBLIC_APP_URL,
      methods: ['GET', 'POST'],
    },
  })
 
  // Socket.io connection handling
  io.on('connection', (socket) => {
    console.log('Client connected:', socket.id)
 
    // Join a room
    socket.on('join-room', (roomId) => {
      socket.join(roomId)
      console.log(`Client ${socket.id} joined room: ${roomId}`)
    })
 
    // Leave a room
    socket.on('leave-room', (roomId) => {
      socket.leave(roomId)
      console.log(`Client ${socket.id} left room: ${roomId}`)
    })
 
    // Handle disconnection
    socket.on('disconnect', () => {
      console.log('Client disconnected:', socket.id)
    })
  })
 
  server.listen(3000, () => {
    console.log('> Ready on http://localhost:3000')
  })
})

Implementing Real-Time Features

1. Live Chat System

Create a chat component:

// components/Chat.tsx
import { useEffect, useState } from 'react'
import { io, Socket } from 'socket.io-client'
import { useSession } from 'next-auth/react'
 
interface Message {
  id: string
  content: string
  sender: string
  timestamp: Date
}
 
export default function Chat({ roomId }: { roomId: string }) {
  const [socket, setSocket] = useState<Socket | null>(null)
  const [messages, setMessages] = useState<Message[]>([])
  const [newMessage, setNewMessage] = useState('')
  const { data: session } = useSession()
 
  useEffect(() => {
    const socket = io(process.env.NEXT_PUBLIC_WS_URL!)
    setSocket(socket)
 
    socket.emit('join-room', roomId)
 
    socket.on('message', (message: Message) => {
      setMessages((prev) => [...prev, message])
    })
 
    return () => {
      socket.emit('leave-room', roomId)
      socket.disconnect()
    }
  }, [roomId])
 
  const sendMessage = (e: React.FormEvent) => {
    e.preventDefault()
    if (!newMessage.trim() || !socket) return
 
    const message: Message = {
      id: Date.now().toString(),
      content: newMessage,
      sender: session?.user?.name || 'Anonymous',
      timestamp: new Date(),
    }
 
    socket.emit('message', { roomId, message })
    setNewMessage('')
  }
 
  return (
    <div className="flex flex-col h-[600px] border rounded-lg">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`flex ${
              message.sender === session?.user?.name
                ? 'justify-end'
                : 'justify-start'
            }`}
          >
            <div
              className={`max-w-[70%] rounded-lg p-3 ${
                message.sender === session?.user?.name
                  ? 'bg-primary text-white'
                  : 'bg-gray-100'
              }`}
            >
              <p className="text-sm font-semibold">{message.sender}</p>
              <p>{message.content}</p>
              <p className="text-xs opacity-70">
                {new Date(message.timestamp).toLocaleTimeString()}
              </p>
            </div>
          </div>
        ))}
      </div>
      <form onSubmit={sendMessage} className="p-4 border-t">
        <div className="flex gap-2">
          <input
            type="text"
            value={newMessage}
            onChange={(e) => setNewMessage(e.target.value)}
            className="flex-1 p-2 border rounded"
            placeholder="Type a message..."
          />
          <button
            type="submit"
            className="px-4 py-2 bg-primary text-white rounded"
          >
            Send
          </button>
        </div>
      </form>
    </div>
  )
}

2. Real-Time Notifications

Implement a notification system:

// components/Notifications.tsx
import { useEffect, useState } from 'react'
import { io } from 'socket.io-client'
import { useSession } from 'next-auth/react'
 
interface Notification {
  id: string
  type: 'info' | 'success' | 'warning' | 'error'
  message: string
  timestamp: Date
}
 
export default function Notifications() {
  const [notifications, setNotifications] = useState<Notification[]>([])
  const { data: session } = useSession()
 
  useEffect(() => {
    const socket = io(process.env.NEXT_PUBLIC_WS_URL!)
 
    socket.emit('join-notifications', session?.user?.id)
 
    socket.on('notification', (notification: Notification) => {
      setNotifications((prev) => [notification, ...prev].slice(0, 5))
    })
 
    return () => {
      socket.emit('leave-notifications', session?.user?.id)
      socket.disconnect()
    }
  }, [session?.user?.id])
 
  return (
    <div className="fixed bottom-4 right-4 space-y-2">
      {notifications.map((notification) => (
        <div
          key={notification.id}
          className={`p-4 rounded-lg shadow-lg ${
            notification.type === 'error'
              ? 'bg-red-500'
              : notification.type === 'warning'
              ? 'bg-yellow-500'
              : notification.type === 'success'
              ? 'bg-green-500'
              : 'bg-blue-500'
          } text-white`}
        >
          <p>{notification.message}</p>
          <p className="text-xs opacity-70">
            {new Date(notification.timestamp).toLocaleTimeString()}
          </p>
        </div>
      ))}
    </div>
  )
}

3. Collaborative Editing

Implement a simple collaborative text editor:

// components/CollaborativeEditor.tsx
import { useEffect, useState } from 'react'
import { io } from 'socket.io-client'
import { useSession } from 'next-auth/react'
 
interface Change {
  position: number
  type: 'insert' | 'delete'
  value?: string
  timestamp: number
}
 
export default function CollaborativeEditor({ documentId }: { documentId: string }) {
  const [content, setContent] = useState('')
  const [socket, setSocket] = useState<any>(null)
  const { data: session } = useSession()
 
  useEffect(() => {
    const socket = io(process.env.NEXT_PUBLIC_WS_URL!)
    setSocket(socket)
 
    socket.emit('join-document', documentId)
 
    socket.on('document-change', (change: Change) => {
      if (change.type === 'insert') {
        setContent((prev) =>
          prev.slice(0, change.position) +
          change.value +
          prev.slice(change.position)
        )
      } else {
        setContent((prev) =>
          prev.slice(0, change.position) +
          prev.slice(change.position + 1)
        )
      }
    })
 
    return () => {
      socket.emit('leave-document', documentId)
      socket.disconnect()
    }
  }, [documentId])
 
  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    const newContent = e.target.value
    const oldContent = content
    const position = e.target.selectionStart
 
    if (newContent.length > oldContent.length) {
      // Insert
      const change: Change = {
        position,
        type: 'insert',
        value: newContent[position - 1],
        timestamp: Date.now(),
      }
      socket.emit('document-change', { documentId, change })
    } else {
      // Delete
      const change: Change = {
        position,
        type: 'delete',
        timestamp: Date.now(),
      }
      socket.emit('document-change', { documentId, change })
    }
 
    setContent(newContent)
  }
 
  return (
    <div className="w-full max-w-4xl mx-auto">
      <textarea
        value={content}
        onChange={handleChange}
        className="w-full h-[500px] p-4 border rounded-lg"
        placeholder="Start typing..."
      />
    </div>
  )
}

Performance Optimization

  1. Connection Pooling: Limit the number of concurrent connections
  2. Message Batching: Combine multiple updates into a single message
  3. Room Management: Only join necessary rooms
  4. Error Handling: Implement reconnection logic
  5. Message Queue: Use a queue for high-volume updates

Best Practices

  1. Security:

    • Authenticate WebSocket connections
    • Validate all messages
    • Implement rate limiting
    • Use secure WebSocket (WSS)
  2. Scalability:

    • Use Redis for horizontal scaling
    • Implement message queuing
    • Monitor connection limits
    • Handle disconnections gracefully
  3. User Experience:

    • Show connection status
    • Implement typing indicators
    • Add message delivery status
    • Handle offline mode

Conclusion

Implementing real-time features with WebSocket in Next.js opens up possibilities for creating engaging, interactive applications. By following this guide, you can build robust real-time features that enhance user experience and enable collaboration.

Remember to:


Let me know if you need help implementing any real-time features!

Thanks for reading! 🚀