typescript 111 lines · 3 tabs

Streaming Server-Sent Events into a Live React List with an EventSource Hook

Shared by codesnips Sep 2026
3 tabs
import { Controller, Sse, Headers } from '@nestjs/common';
import { interval, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

interface FeedPayload {
  id: number;
  message: string;
  at: string;
}

@Controller('feed')
export class EventsController {
  @Sse('stream')
  stream(@Headers('last-event-id') lastEventId?: string): Observable<MessageEvent> {
    let seq = lastEventId ? Number(lastEventId) : 0;

    return interval(1000).pipe(
      map(() => {
        seq += 1;
        const payload: FeedPayload = {
          id: seq,
          message: `tick #${seq}`,
          at: new Date().toISOString(),
        };
        // `id` lets the browser send Last-Event-ID on reconnect
        return {
          id: String(seq),
          data: payload,
        } as MessageEvent;
      }),
    );
  }
}
3 files · typescript Explain with highlit

This snippet shows the full path of a Server-Sent Events feature: a NestJS controller that streams events over a long-lived HTTP connection, a reusable React hook that consumes them via the browser EventSource API, and a small component that renders a live, capped list. SSE is a good fit here because the flow is one-directional server-to-client, works over plain HTTP/1.1, and gives automatic reconnection for free — unlike WebSockets, which add a second protocol and bidirectional complexity that a live feed does not need.

In events.controller.ts, the stream handler uses RxJS interval mapped into MessageEvent objects, which NestJS serializes into the data: frames the browser expects when a route is annotated with @Sse. Each payload carries an incrementing id and an ISO timestamp so the client can dedupe and order. The Last-Event-ID header is read on connect so a reconnecting client can resume from where it left off rather than replaying the whole stream — this is the backbone of SSE's built-in resilience.

In useEventSource.ts, the hook wraps the imperative EventSource in a declarative interface. It parses each message event's JSON, appends to state while capping the buffer with maxItems to avoid unbounded memory growth, and tracks a status so the UI can reflect connecting, open, or error. Crucially it stores the raw EventSource in a ref and closes it in the useEffect cleanup, preventing duplicate connections across re-renders or Strict Mode double-invocation. The browser handles retry timing itself, but the onerror handler surfaces the transient error state without tearing down the socket.

In LiveFeed.tsx, the component simply calls the hook and maps over events. Because the hook owns the buffer cap and connection lifecycle, the component stays purely presentational. A common pitfall the hook guards against is memory leaks from never-closed connections; another is stale closures over setEvents, avoided by using the functional updater form. Reach for this pattern for dashboards, notification feeds, log tails, and progress streams where updates flow one way and eventual reconnection matters more than sub-millisecond latency.


Related snips

Share this code

Here's the card — post it anywhere.

Streaming Server-Sent Events into a Live React List with an EventSource Hook — share card
Link copied