Skip to content

React integration

The SDK is framework-agnostic. The only React-specific concern is cleanup: a client must be disconnected when the owning component unmounts.

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.

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 version ref 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.

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>
}

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.

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.