Events
Most changes to the state start as events from the server: a message arrives, a member joins, someone starts typing. The client applies each event to its state, then calls the listeners your code registered.
Listeners are for side effects, such as playing a sound or logging. To show data, read the stores, which already hold the result of every event.
Subscribing
client.on() hears every event the client receives: the events of every channel it watches, and notifications for the user. Channel.on() hears the events of one channel. Both take an event type and a listener, and return { unsubscribe }:
const newMessages = client.on("message.new", (event) => {
if (event.user?.id !== client.userId) playNotificationSound();
});
const typing = channel.on("typing.start", (event) => {
console.log(`${event.user?.name ?? "Someone"} is typing in ${channel.cid}`);
});
// When the listeners are no longer needed:
newMessages.unsubscribe();
typing.unsubscribe();The type narrows the event: a message.new listener gets the fields of a message.new event. Leave the type out to hear every event, and narrow the event on event.type:
const everything = client.on((event) => {
if (event.type === "member.added")
console.log(`${event.member.user_id} joined ${event.cid}`);
});Don't pass "all" as the type: the listener hears every event at runtime, but its event is typed never. client.off() and Channel.off() also remove a listener. Your own event types are declared on Data conventions.
What the SDK already applies
By the time your listener runs, the client has applied the event to the channel's state. For a message.new event, the message is in the channel's message list. For typing.start, the user is in channel.state.typing. For member.added, the member is in channel.state.members.
So a listener shouldn't apply the event again. If it needs the result, it reads the state.
Two things are worth knowing about timing:
- A message list publishes live changes in batches, so a subscriber to
messagePaginator.statecan hear about a new message a moment after your listener runs. See Throttling and the memory cap. - Channel lists and threads update from the same events, but there's no fixed order between those updates and your listener. To react to a list or a thread changing, subscribe to its store instead of listening for the event.
Events the client raises itself
The client dispatches a few events of its own, which reach the same listeners as the server's:
| Event | When | What it carries |
|---|---|---|
channels.queried |
A channel was queried or watched, or a page of channels was loaded. | queriedChannels.channels: each channel's data from the response, such as channel, messages, members and read, not Channel instances. queriedChannels.isLatestMessageSet: whether those messages are the newest. |
capabilities.changed |
A query or Channel.updatePartial() returned different capabilities for the current user in a channel. channel.state.ownCapabilities holds them already. |
cid, own_capabilities |
connection.recovered |
The client reloaded its active channels and threads after a reconnect. Channel lists refresh alongside and can finish later. | nothing |
message.read_locally |
Channel.markReadLocally() reset the unread count without sending a request. It's for busy channels such as livestreams, where read events are best turned off and readEvents.localUnreadCountEnabled counts unread messages on the client. See Turning read events off. |
cid, last_read_message_id |
live_location_sharing.started |
Channel.sendSharedLocation() started a live share, one with an end_at. See Location sharing. |
message: the message with the share |
live_location_sharing.stopped |
Channel.stopLiveLocationSharing() succeeded. It also ends the share when the channel is no longer watched. See Location sharing. |
live_location: the stopped share |
connection.fallback_activated |
With enableWSFallback, the WebSocket couldn't connect and the client switched to long-polling. |
mode: "longpoll" |
offline_reactions.queried |
With offline support, client.queryReactionsAndHydrate() found cached reactions for the first page, before the request. |
offlineReactions: the cached reactions |
typing.stop is a server event, but the client also raises it when a user who was typing has sent nothing for 7 seconds.
connection.recovered is the one apps use most. When it fires, the client has already reloaded its active channels and threads, so use it to refresh data your app keeps itself:
client.on("connection.recovered", () => {
void refreshOrderStatus();
});What recovery does is on Connection.
Connection changes
The connection's status isn't sent as an event. It's in two stores:
client.wsConnection.state:isHealthysays whether this client's WebSocket is up.client.networkConnection.state:isOnlinesays whether the device has a network. It'sundefinedwhile that's unknown, so testisOnline === false.
const unsubscribe = client.wsConnection.state.subscribeWithSelector(
({ isHealthy }) => ({ isHealthy }),
({ isHealthy }) => showReconnectingBanner(!isHealthy),
);The handler runs once right away, then on every change. Both stores publish a change as soon as it happens, so if a short drop shouldn't show a banner, delay it yourself. The two stores are explained on Connection.