All hooks / Browser APIs
useEventSource
Live updates from the server over Server-Sent Events (EventSource): connects while url is set, closes on unmount or when url changes or is null, and reconnects on its own.
Browsers retry dropped connections themselves, but give up for good when the server answers with an error status; this hook keeps retrying then, waiting 1s, 2s, 4s, up to 30s between attempts. With pauseWhenHidden, a background tab holds no connection.
data is only the latest message: when several arrive at once, a render may see just the last. To keep every message (a feed, a log), use onMessage. Messages are strings; JSON.parse them yourself.
Import
import { useEventSource } from '@vincent.ri/react-tils'
// or only this hook
import { useEventSource } from '@vincent.ri/react-tils/useEventSource'Usage
const { status, data } = useEventSource(`/api/jobs/${id}/progress`)
const percent = data ? JSON.parse(data).percent : 0
return <progress value={percent} max={100} aria-busy={status !== 'open'} />
// Keep every message
const [items, setItems] = useState<Notification[]>([])
useEventSource('/api/notifications', {
onMessage: (event) => setItems((list) => [JSON.parse(event.data), ...list]),
pauseWhenHidden: true,
})Live demo
A service worker stands in for the server: one tick a second.
When to use it
One-way live updates: notifications, progress of a job, live scores or prices, a dashboard, logs.
When not to
Streaming an AI response from a POST; EventSource can only GET and can't set headers, so read fetch()'s body stream (or use your AI SDK). For two-way messaging, use a WebSocket library. To check for changes every minute or so, polling with useInterval is simpler.
API
function useEventSource(url: string | null, options?: EventSourceOptions): EventSourceState| Parameter | Description |
|---|---|
url | The stream's URL, or null to stay disconnected. |
options | onMessage, events, withCredentials, pauseWhenHidden. Optional. |
Returns { status, data }.
Types
export type EventSourceStatus = "connecting" | "open" | "closed";
export type EventSourceOptions = {
/** Called for every message, including named `events`. Can change every render. */
onMessage?: (event: MessageEvent<string>) => void;
/** Named events to receive too (`event: price` in the stream), besides unnamed messages. */
events?: string[];
/** Send cookies to another origin. Default `false`. */
withCredentials?: boolean;
/** Disconnect while the tab is hidden, and reconnect when it is visible again. Default `false`. */
pauseWhenHidden?: boolean;
};
export type EventSourceState = {
/** `'connecting'` (also while retrying), `'open'`, or `'closed'` (no `url`, or paused). */
status: EventSourceStatus;
/** The latest message's `data`, or `null` before the first. Resets when `url` changes. */
data: string | null;
};Browser support
| Uses | Chrome | Edge | Firefox | Safari | Chrome Android | Safari iOS |
|---|---|---|---|---|---|---|
| Page VisibilityWidely available | 33+ | 12+ | 18+ | 7+ | 33+ | 7+ |
| EventSourceWidely available | 6+ | 79+ | 6+ | 5+ | 18+ | 5+ |
First version with support, from MDN's compatibility data (web-features 3.42.0, @mdn/browser-compat-data 8.1.5).
Coming from another library?
Other libraries call this useEventSource (VueUse), useSSE, useServerSentEvents.