Next.js
Real-time Features
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
- Create an SSE endpoint that streams live stock prices
- Build a real-time chat application using WebSockets
- Implement a notification system with Pusher
- Create an online presence indicator showing active users
- Build a collaborative text editor with real-time updates
- Implement typing indicators in a chat application
- Create a live activity feed that updates automatically
- 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.