PathLogs UI
Русский

Hooks

useEventStream

Subscribing to a server event stream, paused while the tab is hidden.

Subscribing to a server event stream (SSE). The typical use is a «live» screen: the server says «something changed» and the page pulls fresh data itself, with no reload and no loss of scroll position.

Example

обновлено в 09:05

Reconnection is not our concern: EventSource handles it. Our job is to be honest about the fact that the connection is currently down.

Options

url*string | null

The stream address. null disables the subscription — handy while the id is not known yet.

onEvent(event: MessageEvent) => void

What to do on an event. Read at event time, so no stable reference is needed.

eventsstring[]

defaults to: ["message"]

The SSE event names to react to.

deferWhenHiddenboolean

defaults to: true

Defer handling while the tab is hidden, then run it once on return.

enabledboolean

defaults to: true

Disable the subscription without removing the hook.

withCredentialsboolean

defaults to: false

Send cookies — needed for authenticated streams on another domain.

Returns { status, updatedAt }, where the status is connecting, live or offline.

Hidden tabs

Only the last event is kept: the screen re-reads its whole state anyway, so there is nothing to gain from accumulating a queue.

The server side

The hook assumes nothing about the server beyond the SSE format. A minimal handler in Next.js:

app/api/projects/[id]/stream/route.ts
export async function GET(req: Request, { params }) {
  const encoder = new TextEncoder();
  let version = await projectVersion(params.id);

  const stream = new ReadableStream({
    async start(controller) {
      const send = (event: string, data: string) =>
        controller.enqueue(encoder.encode(`event: ${event}\ndata: ${data}\n\n`));

      send("sync", version);

      const timer = setInterval(async () => {
        const next = await projectVersion(params.id);
        if (next !== version) {
          version = next;
          send("change", next);
        }
      }, 4000);

      // We close the connection ourselves rather than waiting for the platform
      // limit: the browser reconnects, so the stream is not cut off by a
      // hosting timeout
      setTimeout(() => {
        clearInterval(timer);
        controller.close();
      }, 45_000);
    },
  });

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