@@ -251,13 +251,54 @@ When your OpenAPI spec defines endpoints with `text/event-stream` responses, the
251251
252252### Consuming a stream
253253
254- ``` js
254+ SSE requires a client component since it uses browser APIs and React hooks.
255+
256+ ``` tsx
257+ ' use client' ;
258+
259+ import { useRef , useState } from ' react' ;
255260import { watchStockPrices } from ' ./client/sdk.gen' ;
261+ import type { StockUpdate } from ' ./client/types.gen' ;
262+
263+ export function StockTicker() {
264+ const [updates, setUpdates] = useState <StockUpdate []>([]);
265+ const controllerRef = useRef <AbortController | null >(null );
266+
267+ const connect = async () => {
268+ const controller = new AbortController ();
269+ controllerRef .current = controller ;
270+
271+ try {
272+ const { stream } = await watchStockPrices ({
273+ signal: controller .signal ,
274+ });
275+
276+ for await (const event of stream ) {
277+ setUpdates ((prev ) => [... prev , event ]);
278+ }
279+ } catch {
280+ if (! controller .signal .aborted ) {
281+ console .error (' Stream failed' );
282+ }
283+ }
284+ };
256285
257- const { stream } = await watchStockPrices ();
286+ const disconnect = () => {
287+ controllerRef .current ?.abort ();
288+ controllerRef .current = null ;
289+ };
258290
259- for await (const event of stream ) {
260- console .log (event );
291+ return (
292+ <>
293+ <button onClick = { connect } >Connect</button >
294+ <button onClick = { disconnect } >Disconnect</button >
295+ <ul >
296+ { updates .map ((u , i ) => (
297+ <li key = { i } >{ JSON .stringify (u )} </li >
298+ ))}
299+ </ul >
300+ </>
301+ );
261302}
262303```
263304
@@ -303,6 +344,73 @@ const { stream } = await watchStockPrices({
303344});
304345```
305346
347+ ### Custom hook factory
348+
349+ You can create a reusable factory that wraps any SSE SDK function into a React hook.
350+
351+ ``` tsx
352+ ' use client' ;
353+
354+ import { useCallback , useRef , useState } from ' react' ;
355+
356+ export function createUseSse<
357+ TFn extends (... args : any []) => Promise <{ stream: AsyncGenerator <any > }>,
358+ >(sseFn : TFn ) {
359+ type TEvent = Awaited <ReturnType <TFn >> extends { stream: AsyncGenerator <infer E > } ? E : never ;
360+ type TOptions = Parameters <TFn >[0 ];
361+
362+ return function useSse() {
363+ const [events, setEvents] = useState <TEvent []>([]);
364+ const [status, setStatus] = useState <' connected' | ' disconnected' | ' error' >(' disconnected' );
365+ const controllerRef = useRef <AbortController | null >(null );
366+
367+ const connect = useCallback (async (options ? : Omit <TOptions , ' signal' >) => {
368+ const controller = new AbortController ();
369+ controllerRef .current = controller ;
370+ setStatus (' connected' );
371+ setEvents ([]);
372+
373+ try {
374+ const { stream } = await sseFn ({
375+ ... options ,
376+ signal: controller .signal ,
377+ } as TOptions );
378+
379+ for await (const event of stream ) {
380+ setEvents ((prev ) => [... prev , event as TEvent ]);
381+ }
382+ } catch {
383+ if (! controller .signal .aborted ) {
384+ setStatus (' error' );
385+ return ;
386+ }
387+ }
388+ setStatus (' disconnected' );
389+ }, []);
390+
391+ const disconnect = useCallback (() => {
392+ controllerRef .current ?.abort ();
393+ controllerRef .current = null ;
394+ setStatus (' disconnected' );
395+ }, []);
396+
397+ return { connect , disconnect , events , status };
398+ };
399+ }
400+ ```
401+
402+ ``` tsx
403+ import { watchStockPrices } from ' ./client/sdk.gen' ;
404+ import { createUseSse } from ' ./hooks/createUseSse' ;
405+
406+ const useStockPrices = createUseSse (watchStockPrices );
407+
408+ function StockTicker() {
409+ const { events, status, connect, disconnect } = useStockPrices ();
410+ // ...
411+ }
412+ ```
413+
306414::: tip
307415Request interceptors registered through ` client.interceptors.request ` apply to SSE connections, including on each reconnect attempt.
308416:::
0 commit comments