Skip to main content
Back to Blog
Tutorials
3 min read
November 14, 2024

How to Implement Server-Sent Events Streaming in Next.js

Implement real-time streaming with Server-Sent Events in Next.js for live updates, progress bars, and AI responses.

Ryel Banfield

Founder & Lead Developer

Server-Sent Events (SSE) provide a simple way to stream data from server to client without WebSockets.

When to Use SSE vs WebSockets

  • SSE: One-way server-to-client (notifications, live feeds, AI streaming)
  • WebSockets: Two-way communication (chat, collaborative editing, gaming)

SSE automatically reconnects, works over HTTP/2, and is simpler to implement.

Step 1: Basic SSE Route

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

  const stream = new ReadableStream({
    async start(controller) {
      // Send initial data
      controller.enqueue(
        encoder.encode(`data: ${JSON.stringify({ type: "connected" })}\n\n`)
      );

      // Simulate streaming data
      for (let i = 0; i < 10; i++) {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        controller.enqueue(
          encoder.encode(
            `data: ${JSON.stringify({ type: "update", count: i + 1 })}\n\n`
          )
        );
      }

      // Signal completion
      controller.enqueue(
        encoder.encode(`data: ${JSON.stringify({ type: "done" })}\n\n`)
      );
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

Step 2: Client-Side Hook

"use client";

import { useState, useEffect, useCallback } from "react";

interface SSEOptions {
  onMessage?: (data: unknown) => void;
  onError?: (error: Event) => void;
  onOpen?: () => void;
}

export function useSSE(url: string, options: SSEOptions = {}) {
  const [isConnected, setIsConnected] = useState(false);
  const [data, setData] = useState<unknown[]>([]);

  useEffect(() => {
    const eventSource = new EventSource(url);

    eventSource.onopen = () => {
      setIsConnected(true);
      options.onOpen?.();
    };

    eventSource.onmessage = (event) => {
      const parsed = JSON.parse(event.data);
      setData((prev) => [...prev, parsed]);
      options.onMessage?.(parsed);

      if (parsed.type === "done") {
        eventSource.close();
        setIsConnected(false);
      }
    };

    eventSource.onerror = (error) => {
      options.onError?.(error);
      setIsConnected(false);
    };

    return () => {
      eventSource.close();
      setIsConnected(false);
    };
  }, [url]);

  return { isConnected, data };
}

Step 3: Progress Streaming

// app/api/process/route.ts
export async function POST(req: Request) {
  const { items } = await req.json();
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < items.length; i++) {
        // Process each item
        await processItem(items[i]);

        controller.enqueue(
          encoder.encode(
            `data: ${JSON.stringify({
              type: "progress",
              current: i + 1,
              total: items.length,
              percent: Math.round(((i + 1) / items.length) * 100),
              item: items[i],
            })}\n\n`
          )
        );
      }

      controller.enqueue(
        encoder.encode(`data: ${JSON.stringify({ type: "complete" })}\n\n`)
      );
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
    },
  });
}

Step 4: Progress Bar Component

"use client";

import { useState } from "react";

export function ProgressStream() {
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState("");
  const [isProcessing, setIsProcessing] = useState(false);

  async function startProcessing() {
    setIsProcessing(true);
    setProgress(0);

    const res = await fetch("/api/process", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ items: Array.from({ length: 50 }) }),
    });

    const reader = res.body?.getReader();
    const decoder = new TextDecoder();

    if (!reader) return;

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      const text = decoder.decode(value);
      const lines = text.split("\n").filter((line) => line.startsWith("data: "));

      for (const line of lines) {
        const data = JSON.parse(line.slice(6));

        if (data.type === "progress") {
          setProgress(data.percent);
          setStatus(`Processing ${data.current} of ${data.total}...`);
        } else if (data.type === "complete") {
          setStatus("Complete!");
          setIsProcessing(false);
        }
      }
    }
  }

  return (
    <div className="space-y-4">
      <button
        onClick={startProcessing}
        disabled={isProcessing}
        className="rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
      >
        {isProcessing ? "Processing..." : "Start Processing"}
      </button>

      {isProcessing && (
        <div>
          <div className="h-3 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
            <div
              className="h-full rounded-full bg-blue-600 transition-all duration-300"
              style={{ width: `${progress}%` }}
            />
          </div>
          <p className="mt-1 text-sm text-gray-500">{status}</p>
        </div>
      )}
    </div>
  );
}

Step 5: Live Feed with EventSource

"use client";

import { useSSE } from "@/hooks/useSSE";

export function LiveFeed() {
  const { isConnected, data } = useSSE("/api/stream/feed");

  return (
    <div>
      <div className="mb-2 flex items-center gap-2">
        <span
          className={`h-2 w-2 rounded-full ${
            isConnected ? "bg-green-500" : "bg-gray-400"
          }`}
        />
        <span className="text-sm text-gray-500">
          {isConnected ? "Connected" : "Disconnected"}
        </span>
      </div>

      <div className="space-y-2">
        {data.map((item: any, i: number) => (
          <div
            key={i}
            className="rounded-lg border p-3 animate-in slide-in-from-top-2 dark:border-gray-700"
          >
            <p className="text-sm">{item.message}</p>
            <p className="mt-1 text-xs text-gray-500">
              {new Date(item.timestamp).toLocaleTimeString()}
            </p>
          </div>
        ))}
      </div>
    </div>
  );
}

Step 6: Named Events

// Server: Send named events
controller.enqueue(
  encoder.encode(`event: notification\ndata: ${JSON.stringify(notification)}\n\n`)
);
controller.enqueue(
  encoder.encode(`event: status\ndata: ${JSON.stringify(status)}\n\n`)
);

// Client: Listen to specific events
const eventSource = new EventSource("/api/events");

eventSource.addEventListener("notification", (e) => {
  const data = JSON.parse(e.data);
  showNotification(data);
});

eventSource.addEventListener("status", (e) => {
  const data = JSON.parse(e.data);
  updateStatus(data);
});

Need Real-Time Features?

We build web applications with live streaming, real-time updates, and event-driven architectures. Contact us to discuss your project.

SSEserver-sent eventsstreamingreal-timeNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles