# Streams & Channels

Follow a server-sent event stream, keep a query up to date from one, and send on a WebSocket channel from React components.

## Following a stream

`useSubscription` opens a stream endpoint and follows it for as long as the component is mounted:

```tsx title="components/ticker.tsx"
"use client"

import { q } from "@/lib/query"

export function Ticker({ symbol }: { symbol: string }) {
  const { data, status, error } = q.prices.useSubscription("ticks", { variables: { symbol } })

  if (error) return <p>Price feed unavailable: {error.message}</p>
  if (status === "connecting") return <p>Connecting…</p>
  return <p>{symbol}: {data?.price}</p>
}
```

- `data` — the latest event's payload, or `optimisticData` until the first one arrives.
- `events` — the stream's latest events, oldest first: 100 unless `maxEvents` says otherwise. `maxEvents: Infinity` keeps every event, and the list then grows for as long as the stream runs. A new `maxEvents` applies without opening the stream again.
- `status` — `connecting` from the first render until the stream reports otherwise, then `connected`, `reconnecting` or `disconnected`. It is `disconnected` while `enabled` is `false`, and once the stream could not be opened.
- `error` — why the stream failed or could not be opened; the stream's next event clears it.
- `close()` — closes the stream before the component unmounts.

A change of method or variables opens another stream. `data`, `events`, `error` and `status` start over with it in the same render, so nothing of the previous stream is ever shown as the new one's. `onData`, `onError` and `onStatusChange` are called as the stream delivers an event, fails and changes status — the ones the component last rendered with, so a new handler, or a new `maxEvents`, applies without reopening the stream. The hooks write nothing as a component renders, which keeps them right under the React Compiler: from React 19.2 they hand their handlers to React's `useEffectEvent`.

## Keeping a query up to date

`useComputedQuery` fetches a query and merges a stream of the same group into its cached data:

```tsx title="components/orders.tsx"
"use client"

import { q } from "@/lib/query"

export function Orders() {
  const { data: orders } = q.orders.useComputedQuery("list", {
    subscriptionMethod: "changes",
    mapper: (orders, event) => [...(orders ?? []), event.data],
    onStreamError: (error) => console.warn("Live updates stopped:", error.message),
  })

  return <ul>{orders?.map((order) => <li key={order.id}>{order.total}</li>)}</ul>
}
```

The stream opens together with the query. Events that arrive before the query's first answer are held and merged into it, in order, before it is cached. `subscriptionVariables` opens the stream with other variables than the query's.

A stream that fails, is refused or cannot be opened does not fail the query: the query keeps its data and its `error`, and goes on without live updates. `onStreamError` says why, and `onStreamStatusChange` follows the stream's status.

## Sending on a channel

`useChannel` joins a channel. It returns the last message the server sent on it, and `send` to send one back:

```tsx title="components/room.tsx"
"use client"

import { q } from "@/lib/query"

export function Room({ room }: { room: string }) {
  const { lastMessage, status, send } = q.chat.useChannel("room", { variables: { room } })

  return (
    <div>
      <p>{lastMessage?.text}</p>
      <button disabled={status !== "connected"} onClick={() => send({ text: "hello" })}>
        Say hello
      </button>
    </div>
  )
}
```

`send` answers whether the message went out or waits to go out. A message sent before the channel has opened — from a mount effect, say — waits for it, and one sent while the connection comes back waits in the socket's queue. `send` answers `false`, with a warning, for a message it dropped: the channel is disabled or closed, the server ended it, the queue has no room left, or the message is larger than the socket accepts, which `onError` also reports with code `4413`.

> **send in an effect**
>
> `send` returns a value, and an effect may only return its cleanup, so call it in a block: `useEffect(() => { send(message) }, [send, message])`.

`error` holds the last error on the channel: a `RealtimeError` whose `code` tells a refused message, a failed handler and a channel ended with a reason apart, or the `ConfigurationError` that kept the channel from opening. It is cleared when the channel connects again. Like a stream, a channel starts over — `lastMessage`, `error`, and `status` as `connecting` — when its method or variables change.

A proxy caller opens its channels at `proxyPath.ws`, which has no default: see [createProxyQueryCaller](/docs/api/create-proxy-query-caller).

## A plain caller's status

The hooks above report their own `status`. For a stream or channel opened with a plain caller, `useConnectionStatus` follows it:

```tsx
import { useConnectionStatus } from "@noverax/courier-react"

const { status, isConnected, isReconnecting } = useConnectionStatus(feed)
```

It takes an `SSESubscription` or a `WSChannel` a caller returned, or a `WSConnection`; `null` reads as `disconnected`.

## Next steps

- [Streams & Channels](/docs/advanced/realtime) — serve streams and channels, share a topic, and broadcast to it
- [useQuery](/docs/react-query/use-query) — fetch data with typed queries
