Next.js

Real-time Features

22 min Lesson 28 of 40

Real-time Features

Real-time features enable instant data updates without page refreshes, creating dynamic, interactive experiences. This lesson covers WebSockets, Server-Sent Events (SSE), and popular real-time libraries for Next.js applications.

Understanding Real-time Communication

Three main approaches for real-time communication in web applications:

/* 1. Polling - Client repeatedly requests updates */ Pros: Simple, works everywhere Cons: Inefficient, high latency, server load /* 2. Server-Sent Events (SSE) - Server pushes to client */ Pros: Built-in browser support, automatic reconnection Cons: Unidirectional (server → client only) /* 3. WebSockets - Bidirectional communication */ Pros: Full-duplex, low latency, efficient Cons: More complex setup, requires WebSocket server

Server-Sent Events (SSE)

SSE provides a simple way to push real-time updates from server to client:

SSE API Route

// app/api/events/route.ts export const runtime = 'nodejs'; // SSE requires Node.js runtime export async function GET(request: Request) { const encoder = new TextEncoder(); // Create a custom readable stream const stream = new ReadableStream({ async start(controller) { // Send initial connection message controller.enqueue( encoder.encode(`data: ${JSON.stringify({ type: 'connected' })}\n\n`) ); // Send updates every 3 seconds const interval = setInterval(() => { const data = { type: 'update', timestamp: new Date().toISOString(), value: Math.random(), }; controller.enqueue( encoder.encode(`data: ${JSON.stringify(data)}\n\n`) ); }, 3000); // Clean up on disconnect request.signal.addEventListener('abort', () => { clearInterval(interval); controller.close(); }); }, }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }); }

SSE Client Component

// components/SSEClient.tsx 'use client'; import { useEffect, useState } from 'react'; export default function SSEClient() { const [messages, setMessages] = useState<any[]>([]); const [status, setStatus] = useState('disconnected'); useEffect(() => { // Create EventSource connection const eventSource = new EventSource('/api/events'); eventSource.onopen = () => { setStatus('connected'); }; eventSource.onmessage = (event) => { const data = JSON.parse(event.data); setMessages((prev) => [...prev, data]); }; eventSource.onerror = () => { setStatus('error'); eventSource.close(); }; // Cleanup on unmount return () => { eventSource.close(); }; }, []); return ( <div> <p>Status: {status}</p> <ul> {messages.map((msg, i) => ( <li key={i}>{JSON.stringify(msg)}</li> ))} </ul> </div> ); }

WebSockets in Next.js

WebSockets provide full-duplex communication for real-time features:

WebSocket Server Options: Next.js doesn't include a built-in WebSocket server. You need to use a separate server (custom Node.js server, third-party service like Pusher/Ably, or external WebSocket server).

Custom WebSocket Server

// server.js (custom Next.js server) const { createServer } = require('http'); const { parse } = require('url'); const next = require('next'); const { WebSocketServer } = require('ws'); 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); }); // Create WebSocket server const wss = new WebSocketServer({ server, path: '/api/ws' }); wss.on('connection', (ws) => { console.log('Client connected'); // Send welcome message ws.send(JSON.stringify({ type: 'welcome', message: 'Connected!' })); // Handle incoming messages ws.on('message', (data) => { console.log('Received:', data.toString()); // Broadcast to all clients wss.clients.forEach((client) => { if (client.readyState === 1) { // OPEN client.send(data); } }); }); // Handle disconnect ws.on('close', () => { console.log('Client disconnected'); }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`> Ready on http://localhost:${PORT}`); }); });

WebSocket Client Hook

// hooks/useWebSocket.ts import { useEffect, useRef, useState } from 'react'; export function useWebSocket(url: string) { const [isConnected, setIsConnected] = useState(false); const [messages, setMessages] = useState<any[]>([]); const ws = useRef<WebSocket | null>(null); useEffect(() => { // Create WebSocket connection ws.current = new WebSocket(url); ws.current.onopen = () => { setIsConnected(true); console.log('WebSocket connected'); }; ws.current.onmessage = (event) => { const data = JSON.parse(event.data); setMessages((prev) => [...prev, data]); }; ws.current.onerror = (error) => { console.error('WebSocket error:', error); }; ws.current.onclose = () => { setIsConnected(false); console.log('WebSocket disconnected'); }; // Cleanup on unmount return () => { ws.current?.close(); }; }, [url]); const sendMessage = (message: any) => { if (ws.current?.readyState === WebSocket.OPEN) { ws.current.send(JSON.stringify(message)); } }; return { isConnected, messages, sendMessage }; }

Using the WebSocket Hook

// components/Chat.tsx 'use client'; import { useState } from 'react'; import { useWebSocket } from '@/hooks/useWebSocket'; export default function Chat() { const [input, setInput] = useState(''); const { isConnected, messages, sendMessage } = useWebSocket( 'ws://localhost:3000/api/ws' ); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (input.trim()) { sendMessage({ type: 'message', text: input }); setInput(''); } }; return ( <div> <div>Status: {isConnected ? 'Connected' : 'Disconnected'}</div> <div style={{ height: '300px', overflow: 'auto' }}> {messages.map((msg, i) => ( <div key={i}>{msg.text}</div> ))} </div> <form onSubmit={handleSubmit}> <input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type a message..." /> <button type="submit">Send</button> </form> </div> ); }

Pusher Integration

Pusher is a hosted real-time service that simplifies WebSocket implementation:

Setup Pusher

# Install Pusher SDK npm install pusher pusher-js # Environment variables PUSHER_APP_ID=your_app_id PUSHER_KEY=your_key PUSHER_SECRET=your_secret PUSHER_CLUSTER=your_cluster NEXT_PUBLIC_PUSHER_KEY=your_key NEXT_PUBLIC_PUSHER_CLUSTER=your_cluster

Server-side Pusher Configuration

// lib/pusher.ts import Pusher from 'pusher'; export const pusherServer = new Pusher({ appId: process.env.PUSHER_APP_ID!, key: process.env.PUSHER_KEY!, secret: process.env.PUSHER_SECRET!, cluster: process.env.PUSHER_CLUSTER!, useTLS: true, });

Pusher API Route

// app/api/pusher/message/route.ts import { pusherServer } from '@/lib/pusher'; import { NextRequest } from 'next/server'; export async function POST(request: NextRequest) { const { message, channel } = await request.json(); await pusherServer.trigger(channel, 'new-message', { message, timestamp: new Date().toISOString(), }); return Response.json({ success: true }); }

Client-side Pusher Hook

// hooks/usePusher.ts import { useEffect, useState } from 'react'; import Pusher from 'pusher-js'; export function usePusher(channelName: string, eventName: string) { const [messages, setMessages] = useState<any[]>([]); useEffect(() => { const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, { cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!, }); const channel = pusher.subscribe(channelName); channel.bind(eventName, (data: any) => { setMessages((prev) => [...prev, data]); }); return () => { channel.unbind(eventName); pusher.unsubscribe(channelName); }; }, [channelName, eventName]); return messages; }

Using Pusher in Components

// components/PusherChat.tsx 'use client'; import { useState } from 'react'; import { usePusher } from '@/hooks/usePusher'; export default function PusherChat() { const [input, setInput] = useState(''); const messages = usePusher('chat-room', 'new-message'); const sendMessage = async () => { await fetch('/api/pusher/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channel: 'chat-room', message: input, }), }); setInput(''); }; return ( <div> <div> {messages.map((msg, i) => ( <div key={i}>{msg.message}</div> ))} </div> <input value={input} onChange={(e) => setInput(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && sendMessage()} /> <button onClick={sendMessage}>Send</button> </div> ); }

Socket.io Integration

Socket.io provides robust real-time communication with automatic reconnection:

Socket.io Server Setup

# Install Socket.io npm install socket.io socket.io-client // server.js const { createServer } = require('http'); const { parse } = require('url'); const next = require('next'); const { Server } = require('socket.io'); 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: '*', methods: ['GET', 'POST'], }, }); io.on('connection', (socket) => { console.log('Client connected:', socket.id); socket.on('message', (data) => { io.emit('message', data); }); socket.on('disconnect', () => { console.log('Client disconnected:', socket.id); }); }); server.listen(3000, () => { console.log('> Ready on http://localhost:3000'); }); });

Socket.io Client Hook

// hooks/useSocketIO.ts import { useEffect, useState } from 'react'; import { io, Socket } from 'socket.io-client'; export function useSocketIO() { const [socket, setSocket] = useState<Socket | null>(null); const [isConnected, setIsConnected] = useState(false); useEffect(() => { const socketInstance = io('http://localhost:3000'); socketInstance.on('connect', () => { setIsConnected(true); }); socketInstance.on('disconnect', () => { setIsConnected(false); }); setSocket(socketInstance); return () => { socketInstance.disconnect(); }; }, []); return { socket, isConnected }; }

Real-time Notifications

Implement a notification system with real-time updates:

// components/NotificationBell.tsx 'use client'; import { useEffect, useState } from 'react'; import { usePusher } from '@/hooks/usePusher'; export default function NotificationBell({ userId }: { userId: string }) { const [count, setCount] = useState(0); const [notifications, setNotifications] = useState<any[]>([]); const [isOpen, setIsOpen] = useState(false); const messages = usePusher(`user-${userId}`, 'notification'); useEffect(() => { if (messages.length > 0) { const latest = messages[messages.length - 1]; setNotifications((prev) => [latest, ...prev]); setCount((prev) => prev + 1); } }, [messages]); return ( <div style={{ position: 'relative' }}> <button onClick={() => setIsOpen(!isOpen)}> 🔔 {count > 0 && <span>{count}</span>} </button> {isOpen && ( <div style={{ position: 'absolute', top: '100%', right: 0, background: 'white', border: '1px solid #ccc', minWidth: '250px', }}> {notifications.length === 0 ? ( <p>No notifications</p> ) : ( notifications.map((notif, i) => ( <div key={i} style={{ padding: '10px', borderBottom: '1px solid #eee' }}> {notif.message} </div> )) )} </div> )} </div> ); }

Real-time Presence (Online Users)

Track who's online in real-time:

// components/OnlineUsers.tsx 'use client'; import { useEffect, useState } from 'react'; import Pusher from 'pusher-js'; export default function OnlineUsers() { const [users, setUsers] = useState<string[]>([]); useEffect(() => { const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, { cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!, }); const channel = pusher.subscribe('presence-room'); channel.bind('pusher:subscription_succeeded', (members: any) => { const userList: string[] = []; members.each((member: any) => { userList.push(member.id); }); setUsers(userList); }); channel.bind('pusher:member_added', (member: any) => { setUsers((prev) => [...prev, member.id]); }); channel.bind('pusher:member_removed', (member: any) => { setUsers((prev) => prev.filter((id) => id !== member.id)); }); return () => { pusher.unsubscribe('presence-room'); }; }, []); return ( <div> <h3>Online Users ({users.length})</h3> <ul> {users.map((user) => ( <li key={user}>{user}</li> ))} </ul> </div> ); }
Choosing a Real-time Solution: Use SSE for simple one-way updates (notifications, live feeds). Use WebSockets for bidirectional communication (chat, collaboration). Use hosted services (Pusher, Ably) for production apps to avoid managing infrastructure.

Exercise: Build Real-time Features

  1. Create an SSE endpoint that streams live stock prices
  2. Build a real-time chat application using WebSockets
  3. Implement a notification system with Pusher
  4. Create an online presence indicator showing active users
  5. Build a collaborative text editor with real-time updates
  6. Implement typing indicators in a chat application
  7. Create a live activity feed that updates automatically
  8. Build a real-time dashboard with live metrics
Production Considerations: Implement reconnection logic, handle network failures gracefully, rate-limit connections, authenticate WebSocket connections, and monitor connection counts to prevent resource exhaustion.