Hooks
Every hook requires a CentrifugeProvider above it and throws an error if one is missing.
This package exports three hooks of its own. They reach Centrifugo's native SDK surface. The rest are re-exported from @priemskiyyy/simulcast-react unchanged, so importing them from either package gives the same functions.
| Hook | Returns | Opens a subscription | From |
|---|---|---|---|
useCentrifuge | Centrifuge | null | no | this package |
useSubscriptionEvent | nothing | yes | this package |
useClientEvent | nothing | no | this package |
useChannel | nothing | yes | the runtime |
useChannelDemand | nothing | yes | the runtime |
useChannelStatus | ChannelStatus | no | the runtime |
useConnectionState | ConnectionState | no | the runtime |
Callbacks always see current render values without recreating the subscription, so you never need to memoise them. They may be async, but publications are not queued behind their completion.
Native types
The runtime reports native values as unknown until an application says which client it builds. This package only ever builds a Centrifugo one, so register its type once, anywhere your typechecker sees:
import type { CentrifugeRealtimeClient } from "react-centrifugo";
declare module "@priemskiyyy/simulcast-react" {
interface Register {
client: CentrifugeRealtimeClient;
}
}A publication's native then reads as Centrifugo's PublicationContext instead of unknown. Everything runs the same without it; only the types change. useCentrifuge is typed either way.
useCentrifuge
useCentrifuge(): Centrifuge | nullThe current native client. It re-renders when the client is created, replaced, or released, and reads null before session setup, during server rendering, and while the session is disabled.
const client = useCentrifuge();
const send = async (text: string) => {
if (client === null) {
return;
}
await client.publish("rooms:demo", { text });
};The same client survives connection loss and reconnects, so this value changes on session replacement rather than on every transition. Use useConnectionState to watch those. Effects that use the client should list it as a dependency.
Use it for publish, history, presence, and rpc. Leave the connection and hook-owned subscriptions to the provider; changing their lifecycle directly conflicts with mounted hooks.
useSubscriptionEvent
useSubscriptionEvent<TEvent extends keyof SubscriptionEvents>(
channel: string,
event: TEvent,
onEvent: (context: Parameters<SubscriptionEvents[TEvent]>[0]) => void | Promise<unknown>,
options?: { enabled?: boolean },
): voidAn escape hatch onto the native subscription events: subscribed, error, state, publication, join, and leave. It binds to the subscription the runtime owns and forwards Centrifuge's context untouched.
A native subscription exists only while something demands one, so this hook demands the channel itself. Listening for state on a channel opens a subscription to it.
Name the context type by importing it from centrifuge:
import type { SubscribedContext } from "centrifuge";
const handleSubscribed = (context: SubscribedContext) => {
console.log(context.wasRecovering, context.recovered);
};
useSubscriptionEvent("rooms:demo", "subscribed", handleSubscribed);For the recovery outcome alone, useChannelStatus(channel).recovered is simpler. Use this hook for the SDK's own context.
useClientEvent
useClientEvent<TEvent extends keyof ClientEvents>(
event: TEvent,
onEvent: (context: Parameters<ClientEvents[TEvent]>[0]) => void | Promise<unknown>,
): voidNative client events: connected, disconnected, connecting, error, state, and server-side subscription events including server publications. Client events are global, so this opens nothing.
Listeners attach once the client exists, which is after the provider starts its session. A transition the SDK emits synchronously during that startup happens before any listener is attached. Read useConnectionState for the current value rather than reconstructing it from events.
useChannel
useChannel<TData = unknown>(
channel: string,
onEvent: PublicationHandler<TData>,
options?: { enabled?: boolean; parse?: (data: unknown) => TData },
): voidSubscribes to a channel and receives its publications.
useChannel("rooms:demo", (message, publication) => {
console.log(message, publication.native.offset);
});The handler's second argument is the runtime publication:
type Publication = {
data: unknown;
event?: string;
native: PublicationContext;
};Centrifugo's own PublicationContext, with its offset, tags, and client info, is under native, typed once the client type is registered. Centrifugo publications carry no provider event name, so event is absent.
parse runs on every publication and its return type drives inference. The explicit generic declares your wire contract instead, and nothing validates it at runtime. Supplying both requires them to agree.
useChannelDemand
useChannelDemand(channel: string, options?: { enabled?: boolean }): voidHolds a channel's native subscription open without consuming publications. Use it when a component watches the subscription through the SDK but reads nothing through the hooks. A component that already calls useChannel needs nothing extra.
useChannelStatus
useChannelStatus(
channel: string,
onChange?: (status: ChannelStatus) => void | Promise<unknown>,
): ChannelStatustype ChannelStatus = {
state: "detached" | "unsubscribed" | "subscribing" | "subscribed";
error: { error: unknown } | null;
recovered: boolean;
};Observing a channel's status does not subscribe to it. A channel nobody has subscribed to reports detached, which is distinct from the SDK's unsubscribed: detached means no consumer has opened a subscription, so there is no SDK state to report. If your status stays detached, nothing is demanding that channel.
error carries the provider's own error untyped, so narrow it before reading fields. Errors and recovery shows the narrowing.
recovered reports whether Centrifugo replayed the publications missed since the last subscription. It is false on a first subscription and whenever recovery did not happen, so treat false as a gap to refetch rather than proof of loss.
The optional callback fires on changes after registration; it never replays the current value. Read the return value for that.
useConnectionState
useConnectionState(
onChange?: (state: ConnectionState) => void | Promise<unknown>,
): "disconnected" | "connecting" | "connected"The client's connection state, or disconnected when no session is active.
For transitions with both the old and the new value, use useClientEvent("state", ...).