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
- Setting Up WebSocket Server
- Implementing Real-Time Features
- Performance Optimization
- Best Practices
- Conclusion
Understanding Real-Time Communication
Real-time features are essential for modern web applications:
- Instant messaging and chat
- Live notifications
- Collaborative editing
- Real-time analytics
- Live updates and feeds
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
- Connection Pooling: Limit the number of concurrent connections
- Message Batching: Combine multiple updates into a single message
- Room Management: Only join necessary rooms
- Error Handling: Implement reconnection logic
- Message Queue: Use a queue for high-volume updates
Best Practices
-
Security:
- Authenticate WebSocket connections
- Validate all messages
- Implement rate limiting
- Use secure WebSocket (WSS)
-
Scalability:
- Use Redis for horizontal scaling
- Implement message queuing
- Monitor connection limits
- Handle disconnections gracefully
-
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:
- Test thoroughly
- Monitor performance
- Handle edge cases
- Implement proper error handling
- Consider scalability from the start
Let me know if you need help implementing any real-time features!
Thanks for reading! 🚀