Chuyển đến nội dung chính

Bài 17: Real-time & WebSockets

Server-Sent Events (SSE). WebSockets với Socket.io. Real-time notifications, chat. Pusher, Ably integration. Optimistic UI patterns.

💻 Lập trình — Bài 17 Bài 17: Real-time & WebSockets

React & Next.js: Từ Cơ bản đến Nâng cao

Phần 5: Full-Stack Features

xdev.asia

1. Server-Sent Events (SSE)

// app/api/events/route.ts
export async function GET() {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    start(controller) {
      const interval = setInterval(() => {
        const data = JSON.stringify({
          time: new Date().toISOString(),
          message: 'Server update',
        });
        controller.enqueue(encoder.encode(`data: ${data}\n\n`));
      }, 3000);

      // Cleanup khi client disconnect
      return () => clearInterval(interval);
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  });
}
// Client component
'use client';
import { useEffect, useState } from 'react';

export function LiveUpdates() {
  const [messages, setMessages] = useState<string[]>([]);

  useEffect(() => {
    const eventSource = new EventSource('/api/events');

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setMessages(prev => [...prev, data.message]);
    };

    eventSource.onerror = () => {
      eventSource.close();
    };

    return () => eventSource.close();
  }, []);

  return (
    <ul>
      {messages.map((msg, i) => (
        <li key={i}>{msg}</li>
      ))}
    </ul>
  );
}

2. WebSockets với Socket.io

// server.ts — Custom server cho WebSocket
import { createServer } from 'http';
import { Server } from 'socket.io';
import next from 'next';

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  const httpServer = createServer((req, res) => handle(req, res));
  const io = new Server(httpServer, {
    cors: { origin: '*' },
  });

  io.on('connection', (socket) => {
    console.log('Client connected:', socket.id);

    socket.on('join-room', (room: string) => {
      socket.join(room);
    });

    socket.on('send-message', (data: { room: string; message: string }) => {
      io.to(data.room).emit('new-message', {
        id: Date.now(),
        text: data.message,
        sender: socket.id,
        timestamp: new Date().toISOString(),
      });
    });

    socket.on('disconnect', () => {
      console.log('Client disconnected:', socket.id);
    });
  });

  httpServer.listen(3000);
});

3. Chat Component

'use client';
import { useEffect, useState, useRef } from 'react';
import { io, Socket } from 'socket.io-client';

type Message = {
  id: number;
  text: string;
  sender: string;
  timestamp: string;
};

export function Chat({ room }: { room: string }) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const socketRef = useRef<Socket | null>(null);

  useEffect(() => {
    const socket = io();
    socketRef.current = socket;
    socket.emit('join-room', room);

    socket.on('new-message', (message: Message) => {
      setMessages(prev => [...prev, message]);
    });

    return () => { socket.disconnect(); };
  }, [room]);

  const sendMessage = () => {
    if (!input.trim()) return;
    socketRef.current?.emit('send-message', { room, message: input });
    setInput('');
  };

  return (
    <div>
      <div className="h-96 overflow-y-auto">
        {messages.map(msg => (
          <div key={msg.id}>
            <strong>{msg.sender}:</strong> {msg.text}
          </div>
        ))}
      </div>
      <div className="flex gap-2">
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && sendMessage()}
          placeholder="Nhập tin nhắn..."
        />
        <button onClick={sendMessage}>Gửi</button>
      </div>
    </div>
  );
}

4. Pusher Integration

// lib/pusher.ts
import PusherServer from 'pusher';
import PusherClient from 'pusher-js';

export const pusherServer = new PusherServer({
  appId: process.env.PUSHER_APP_ID!,
  key: process.env.NEXT_PUBLIC_PUSHER_KEY!,
  secret: process.env.PUSHER_SECRET!,
  cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
  useTLS: true,
});

export const pusherClient = new PusherClient(
  process.env.NEXT_PUBLIC_PUSHER_KEY!,
  { cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER! }
);
// Server Action — trigger event
'use server';
import { pusherServer } from '@/lib/pusher';

export async function sendNotification(userId: string, message: string) {
  await pusherServer.trigger(`user-${userId}`, 'notification', {
    message, timestamp: new Date().toISOString(),
  });
}
// Client — listen for events
'use client';
import { useEffect } from 'react';
import { pusherClient } from '@/lib/pusher';

export function Notifications({ userId }: { userId: string }) {
  useEffect(() => {
    const channel = pusherClient.subscribe(`user-${userId}`);
    channel.bind('notification', (data: { message: string }) => {
      // Show toast notification
      alert(data.message);
    });
    return () => { pusherClient.unsubscribe(`user-${userId}`); };
  }, [userId]);

  return null;
}

Bài tiếp theo: Email, Payment & Third-party — Resend, Stripe, OAuth.