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
import { useEventStream } from "@toimetdev/pathlogs-hooks";
import { LiveIndicator } from "@toimetdev/pathlogs-core";
const { status, updatedAt } = useEventStream(`/api/projects/${id}/stream`, {
events: ["change"],
onEvent: () => router.refresh(),
});
<LiveIndicator status={status} updatedAt={updatedAt} locale="en-US" />Reconnection is not our concern: EventSource handles it. Our job is to be honest about the fact that the connection is currently down.
Options
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
events | string[] | ["message"] | The SSE event names to react to. |
deferWhenHidden | boolean | true | Defer handling while the tab is hidden, then run it once on return. |
enabled | boolean | true | Disable the subscription without removing the hook. |
withCredentials | boolean | false | Send cookies — needed for authenticated streams on another domain. |
url*string | nullThe stream address. null disables the subscription — handy while the id is not known yet.
onEvent(event: MessageEvent) => voidWhat 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.
deferWhenHiddenbooleandefaults to: true
Defer handling while the tab is hidden, then run it once on return.
enabledbooleandefaults to: true
Disable the subscription without removing the hook.
withCredentialsbooleandefaults 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:
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" },
});
}