React integration
The SDK is framework-agnostic. The only React-specific concern is cleanup: a client must be disconnected when the owning component unmounts.
Minimal hook
Section titled “Minimal hook”import { useEffect, useState } from 'react'import { createClient, type SportEvent, type SportEventId } from 'realtimeodds'
export function useRealtimeOdds(apiKey: string) { const [events, setEvents] = useState<ReadonlyMap<SportEventId, SportEvent>>(new Map()) const [connected, setConnected] = useState(false)
useEffect(() => { const client = createClient({ url: 'wss://api.realtimeodds.xyz', apiKey })
const refresh = () => setEvents(new Map(client.snapshot().sportEvents))
client.on('connected', () => { setConnected(true); refresh() }) client.on('disconnected', () => setConnected(false)) client.on('sportEvent:added', refresh) client.on('sportEvent:updated', refresh) client.on('sportEvent:removed', refresh)
client.connect().catch(err => console.error('connect failed:', err))
return () => { void client.disconnect() } }, [apiKey])
return { events, connected }}The cleanup function returned by useEffect runs on unmount and when apiKey changes — both should disconnect the previous client.
Why new Map(...) for snapshots
Section titled “Why new Map(...) for snapshots”client.snapshot().sportEvents is a live ReadonlyMap reference. React’s bail-out comparison (===) won’t trigger a re-render when its contents change, because the reference is the same. Either:
- Copy on read:
setEvents(new Map(client.snapshot().sportEvents))(above), or - Track a counter or
versionref and force re-renders externally.
The first is simpler and works fine until you have thousands of events; the second is a perf optimisation for very large grids.
Per-event subscription
Section titled “Per-event subscription”For grids with thousands of rows, subscribing the whole list to every event is wasteful. Have each row component subscribe to its own id via odds:changed:
function PriceCell({ client, selectionId }: { client: Client; selectionId: SelectionId }) { const [price, setPrice] = useState<number | undefined>()
useEffect(() => { const handler = (payload: { selectionId: SelectionId; quote: { price: number } }) => { if (payload.selectionId === selectionId) setPrice(payload.quote.price) } client.on('odds:changed', handler) return () => client.off('odds:changed', handler) }, [client, selectionId])
return <span>{price ?? '—'}</span>}Strict mode
Section titled “Strict mode”React 18 strict mode mounts each effect twice in dev. Your useEffect connect/disconnect cycle runs twice on mount — that’s fine, the SDK’s disconnect() is idempotent and will tear down the first client before the second connects. The pattern above is strict-mode-safe.
Sharing one client across the tree
Section titled “Sharing one client across the tree”Avoid spinning up multiple clients per app — one connection, shared via React context, is cheaper:
const ClientContext = React.createContext<Client | null>(null)
export function RealtimeOddsProvider({ children, apiKey }: { children: ReactNode; apiKey: string }) { const [client, setClient] = useState<Client | null>(null)
useEffect(() => { const c = createClient({ url: 'wss://api.realtimeodds.xyz', apiKey }) c.connect().then(() => setClient(c)).catch(console.error) return () => { void c.disconnect() } }, [apiKey])
return <ClientContext.Provider value={client}>{children}</ClientContext.Provider>}
export const useRealtimeOddsClient = () => useContext(ClientContext)Children read the client and subscribe to whatever they need.
The SDK uses WebSocket, which doesn’t exist in Node SSR contexts (older Node versions) — guard your effects with typeof window !== 'undefined' checks if you target SSR frameworks like Next.js. Effects run client-side only, so useEffect already does the right thing.