---
title: React channels
description: Combine authoritative route data with typed live updates in React.
pagination: false
sidebar:
  icon: panels-top-left
---

Subscribe a React component to one channel partition with `useChannel()`. RELKIT shares equivalent subscriptions by channel name and canonical parameters, so multiple components do not create duplicate transport subscriptions.

The component still owns its application state. A channel callback may patch local state, update the TanStack Query cache, or invalidate an ordinary route query.

## Subscribe to a channel

> **Before you begin**
>
> Complete [frontend package installation and contract pull](/docs/modules/client), then configure
> the backend's selected [realtime provider](/docs/modules/realtime/providers). The frontend client
> does not install backend Redis, filesystem profiles or an agent engine.

The [`defineChannel` announcement example](/docs/modules/realtime#declare-a-channel) generates
`announcements.feed`, its empty partition parameters, and the `posted` event. This component combines
that subscription with the authoritative `GET /announcements` query:

```tsx title="src/Announcements.tsx"
"use client";

/** Combines the commerce announcement query with channel invalidation and reconnect recovery. */
import { useChannel, useRoute } from "@relkit/client/react";

/** Restores authoritative messages on publication, catch-up, and replay gaps.
 * @returns The current messages with query failure and channel status feedback. */
export function Announcements() {
  const messages = useRoute("GET /announcements", { input: {} });
  const channel = useChannel("announcements.feed", {
    params: {},
    on: {
      posted: () => {
        void messages.refetch();
      },
    },
    onCaughtUp: async () => {
      await messages.refetch();
    },
    onGap: async () => {
      await messages.refetch();
    },
  });

  if (messages.isError) return <p role="alert">{messages.error.message}</p>;
  return (
    <section>
      <p role="status">Channel: {channel.status}</p>
      {messages.isPending && <p>Loading messages…</p>}
      <ul>
        {messages.data?.messages.map((message, index) => (
          <li key={index}>{message}</li>
        ))}
      </ul>
    </section>
  );
}
```

The generated registry determines the allowed channel names, partition parameters, event names, payloads, and presence result. Misspelled events and invalid payload assumptions fail during type checking.

Render it under the [Frontend guide's provider](/guides/frontend/connect-react) after pulling a
contract containing those operations. See [`useChannel`](/docs/api/client#usechannel) and
[`useRealtime`](/docs/api/client#userealtime) for the exact hook contracts.

## `useChannel()` options

| Option       | Purpose                                                                                                 |
| ------------ | ------------------------------------------------------------------------------------------------------- |
| `params`     | Selects one validated channel partition.                                                                |
| `on`         | Maps declared event names to typed callbacks. Omitted events are ignored.                               |
| `onCaughtUp` | Runs after RELKIT has attached to the live tail and applied any valid reconnect replay.                 |
| `onGap`      | Restores authoritative state when replay cannot bridge the previous checkpoint. It may be asynchronous. |

The result exposes:

| Field        | Meaning                                                                  |
| ------------ | ------------------------------------------------------------------------ |
| `status`     | Transport state: `idle`, `connecting`, `connected`, `stale`, or `error`. |
| `checkpoint` | Last applied provider checkpoint, when one exists.                       |
| `caughtUp`   | Whether live delivery is safe to present after catch-up or gap recovery. |
| `gap`        | The latest recovery reason, when replay could not continue.              |
| `presence`   | Present only when the channel declares count or member presence.         |

`caughtUp` does not mean your database query has loaded. It means RELKIT has completed the subscription handoff. Coordinate it with the loading state of the route query that owns the page's current data.

## Patch or invalidate?

Patch local state when an event carries the complete change and applying it is simple and deterministic. Invalidate the route query when permissions, derived fields, ordering, or server-side calculations could make a local patch incomplete.

`onCaughtUp` is a useful final invalidation point: load the route, attach the channel, then confirm that no update was missed between those operations. `onGap` must perform a full restoration before it resolves.

## Use the lower-level manager

`useRealtime()` exposes the shared connection status and four typed operations:

| Operation                           | Behavior                                                                   |
| ----------------------------------- | -------------------------------------------------------------------------- |
| `subscribe(name, params, listener)` | Starts or joins a shared subscription and returns an unsubscribe function. |
| `bind(...)`                         | Alias of `subscribe()`.                                                    |
| `unsubscribe(stop)`                 | Calls the returned unsubscribe function.                                   |
| `unbind(stop)`                      | Alias of `unsubscribe()`.                                                  |

Prefer `useChannel()` for components because it handles subscription cleanup and exposes catch-up state. Use `useRealtime()` for an application-level adapter or store that already owns its own lifecycle.

## Mount and identity behavior

The hook waits until `RelkitClientProvider` has a compatible contract and resolved identity. `identityKey: null` blocks private restoration while application identity is unknown. Changing identity or session aborts the old subscription and rejects late frames from it.

Unmounting the final listener releases the shared subscription. It does not delete authoritative application data from your QueryClient.

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/realtime` API](/docs/api/realtime)
- [`@relkit/client` API](/docs/api/client)

- [`examples/commerce-web/app/page.tsx`](https://github.com/rel-kit/relkit/blob/main/examples/commerce-web/app/page.tsx)
