DH
5 min read

Real-Time Updates with Server-Sent Events (SSE) in Next.js 15

Stream multiple named events from a Next.js 15 endpoint and consume them via the useEventSource hook from react-use-websocket.

nextjssse

Server-Sent Events (SSE) provide a straightforward way to push server updates to the client over HTTP—no complex WebSocket handshake required. In this tutorial, we’ll create an SSE endpoint in Next.js 15 (with the App Router) that sends multiple named events—think “news” headlines and “stats” updates—and consume them in the browser with the useEventSource hook from the react-use-websocket library.

1. Project Setup

Create a new Next.js 15 project with the App Router, a src directory, and TypeScript:

npx create-next-app@latest multi-event-sse-app \
--app \
--src-dir \
--typescript

This creates a folder structure like:

multi-event-sse-app/
├─ src/
│ ├─ app/
│ │ ├─ layout.tsx
│ │ ├─ page.tsx
│ │ └─ api/ (we'll put our SSE route here)
├─ next.config.ts
├─ package.json
├─ tsconfig.json
└─ ...

Then install react-use-websocket:

cd multi-event-sse-app
npm install react-use-websocket

2. Creating an SSE Endpoint

We’ll create a streaming endpoint that periodically sends two different named events:

  1. news: Simulated “breaking news” headlines.
  2. stats: Random numeric data, like “active users” or “sales figures.”

Create a file at src/app/api/stream/route.ts:

// src/app/api/stream/route.ts

import { NextRequest, NextResponse } from "next/server";

// Some sample news headlines and stats for variety
const HEADLINES = [
"New Study Reveals Benefits of Walking Daily",
"Tech Startups Rally in Surging Market",
"Local Basketball Team Clinches Playoffs",
"Farmers Embrace High-Tech Irrigation Methods",
];
const MAX_HEADLINES = HEADLINES.length;

export async function GET(_req: NextRequest) {
const encoder = new TextEncoder();

const readableStream = new ReadableStream({
start(controller) {
let newsIndex = 0;

const intervalId = setInterval(() => {
// 1) Send a "news" event with a random headline
const randomHeadline =
HEADLINES[Math.floor(Math.random() * MAX_HEADLINES)];
const newsChunk = `event: news\ndata: ${JSON.stringify({
headline: randomHeadline,
})}\n\n`;
controller.enqueue(encoder.encode(newsChunk));

// 2) Send a "stats" event with random numbers
const activeUsers = Math.floor(Math.random() * 1000);
const sales = (Math.random() * 10000).toFixed(2);
const statsChunk = `event: stats\ndata: ${JSON.stringify({
activeUsers,
sales,
})}\n\n`;
controller.enqueue(encoder.encode(statsChunk));

newsIndex++;
}, 3000);

return () => {
clearInterval(intervalId);
};
},
});

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

Key Points

  • Multiple Named Events: event: news and event: stats appear before each data: line. This is how SSE categorizes event types.
  • Data Payloads: We’re sending different shapes of JSON for each event type (one for “headline,” one for “activeUsers” and “sales”).
  • Interval: Every 3 seconds, we send one “news” event and one “stats” event. Adjust timing to your needs.

Now, at http://localhost:3000/api/stream, you’ll see a continuous stream of SSE events labeled “news” and “stats.”

3. Consuming SSE with useEventSource

In Next.js 15, files in app/ are server components by default. We need a client component to work with browser APIs like SSE. Let’s convert our home page (page.tsx) into a client component and use useEventSource from react-use-websocket.

Create or edit src/app/page.tsx:

"use client";

import { useState } from "react";
import { useEventSource } from "react-use-websocket";

export default function HomePage() {
// We'll store news headlines and stats updates separately
const [headlines, setHeadlines] = useState<string[]>([]);
const [statsLog, setStatsLog] = useState<{ activeUsers: number; sales: string }[]>([]);

const { getEventSource, readyState } = useEventSource("/api/stream", {
// If needed, pass credentials or custom headers here:
// withCredentials: true,
// events mapping: handle named events
events: {
news: (evt) => {
try {
const payload = JSON.parse(evt.data);
setHeadlines((prev) => [...prev, payload.headline]);
} catch (error) {
console.error("Failed to parse news payload:", error);
}
},
stats: (evt) => {
try {
const payload = JSON.parse(evt.data);
setStatsLog((prev) => [...prev, payload]);
} catch (error) {
console.error("Failed to parse stats payload:", error);
}
},
},
});

// Convert numeric readyState to human-readable status
// 0 = CONNECTING, 1 = OPEN, 2 = CLOSED
const connectionStatus = ["Connecting", "Open", "Closed"][readyState] || "Unknown";

return (
<main style={{ padding: "1rem" }}>
<h1>SSE Demo: News & Stats</h1>
<p>Connection: {connectionStatus}</p>

<section style={{ marginTop: "1rem" }}>
<h2>Latest Headlines</h2>
<ul>
{headlines.map((headline, idx) => (
<li key={idx}>{headline}</li>
))}
</ul>
</section>

<section style={{ marginTop: "1rem" }}>
<h2>Stats Log</h2>
<ul>
{statsLog.map((entry, idx) => (
<li key={idx}>
Active Users: {entry.activeUsers}, Sales: ${entry.sales}
</li>
))}
</ul>
</section>
</main>
);
}

How useEventSource Works

  • useEventSource(endpoint, options):
    1. endpoint is our SSE endpoint (/api/stream).
    2. options.events is an object that maps named event types (news, stats) to callback functions.
  • Named Event Handlers: If the SSE chunk has event: news, it calls the news callback; if it’s event: stats, it calls stats.
  • readyState: 0 = CONNECTING, 1 = OPEN, 2 = CLOSED.

4. Trying It Out

  1. Start the dev server:
    npm run dev
  2. Visit http://localhost:3000.
  3. Every 3 seconds, you’ll see:
    • One “news” event with a random headline, appended to the “Latest Headlines” list.
    • One “stats” event with random “activeUsers” and “sales,” appended to the “Stats Log.”

If you open the Network panel in your browser’s DevTools, you’ll see the continuous text/event-stream response from /api/stream.

5. Expanding the Example

  • Multiple Frequencies: You could send “news” less often and “stats” more often by maintaining separate intervals or randomized intervals.
  • Custom Event Types: Add more events, like “alerts,” “notifications,” or “chat.”
  • Error Handling: If your stream requires authentication or tokens, handle them in the options config or query parameters.

6. Conclusion

By combining SSE with Next.js 15’s App Router and the useEventSource hook from react-use-websocket, you can:

  1. Push real-time data to connected clients with minimal overhead—no handshake complexities like WebSockets.
  2. Use named events (news, stats, etc.) to categorize incoming data cleanly.
  3. Easily parse JSON in your client’s callback functions for specialized updates.

Key Takeaways:

  • Endpoint: A streaming route.ts that sends SSE data with event: <name> lines.
  • Client: A "use client" component with useEventSource, which neatly handles multiple event types.
  • Versatility: Perfect for live dashboards, notifications, or any scenario needing continuous one-way updates from server to client.

Experiment with authenticating requests, sending complex JSON, or hooking up a real data source for production-level streaming applications.

Damian Hodgkiss

Damian Hodgkiss

Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.

Creating Freedom

Join me on the journey from engineer to solopreneur. Learn how to build profitable SaaS products while keeping your technical edge.

    Proven strategies

    Learn the counterintuitive ways to find and validate SaaS ideas

    Technical insights

    From choosing tech stacks to building your MVP efficiently

    Founder mindset

    Transform from engineer to entrepreneur with practical steps