What's optimistic and what isn't
An optimistic action shows its result before the server confirms it. In stream-chat, the message operations, poll votes and drafts are optimistic, and conversation actions aren't. This page lists which is which, and what happens when the server answers or a request fails. Use it to decide where your UI needs a pending state of its own: for the actions whose result appears only when the server answers.
Example
The user likes a message, then archives the channel:
- The like appears on the message at once.
channel.messageOperations.addReaction()changed the message before sending the request. - The server answers, and its copy of the message, with its reaction counts, replaces the local one.
- The channel doesn't look archived yet.
Channel.archive()only sends the request. - The server sends a
member.updatedevent. It updates the user's membership inchannel.state, and the channel now looks archived, in every list that shows it.
Optimistic actions
The message operations change the message list before the request goes out: sending, retrying, editing and deleting a message, and adding or removing a reaction. Optimistic updates lists what each one shows at once and what's undone when its request fails. The composer's MessageComposer.send() and MessageComposer.update() go through them, so they behave the same way. The composer also clears itself before the request goes out. The SDK's message actions go through them too.
These actions are optimistic too:
| Action | Shown at once | When the request fails |
|---|---|---|
Poll.castVote() and Poll.removeVote() |
the vote cast or removed | undone once all pending vote changes have settled |
Poll.close() |
the poll as closed | reopened, unless the server confirmed the close in the meantime |
MessageComposer.createDraft() and MessageComposer.deleteDraft() |
the draft is the composer's content, so it's already shown | nothing is undone |
Poll votes are sent one at a time, in the order they were made. The server's vote counts are shown once the events of all pending votes have arrived. If an event is lost, the client catches up after 3 seconds. When the user has already used all their votes, Poll.castVote() changes nothing and publishes a validation:poll:castVote:limit notification.
With offline support, an operation that fails because the device is offline waits in the offline queue and is sent after the next reconnect. An edit, a delete or a reaction that waits there isn't failed but pending, so its local change stays. A send that waits there is still marked failed, because retrying is how the user gets it out before the reconnect. A draft that waits there stays in the composer, as it does after any failure.
How each operation works is on Message operations.
Conversation actions
Actions on a channel, a membership or a user aren't optimistic, and neither is pinning a message. Their effect appears when the server's event or response arrives, not when the call is made.
Actions that wait for the server
| Action | Local state changes when |
|---|---|
Channel.markRead(), Channel.markReadViaReporter(), Thread.markRead() |
the message.read or notification.mark_read event updates the read state |
Channel.markUnread() |
the notification.mark_unread event updates the read state |
Channel.archive(), Channel.unarchive(), Channel.pin() and Channel.unpin() |
the member.updated event updates channel.state.membership |
Channel.mute() and Channel.unmute() |
the notification.channel_mutes_updated event updates channel.state.muteStatus |
client.pinMessage() and client.unpinMessage() |
the message.updated event updates the message |
Channel.hide() and Channel.show() |
the channel.hidden or channel.visible event updates channel.data.hidden |
Channel.update(), Channel.updatePartial(), Channel.addMembers(), Channel.removeMembers() |
the response updates channel.data, and the member events update channel.state.members |
client.blockUser() and client.unblockUser() |
the response arrives, and client.blockedUsers is updated |
client.reminders.createReminder(), client.reminders.updateReminder() and client.reminders.deleteReminder() |
the response arrives, and the reminder events after it |
If such a request fails, nothing has changed locally, so there is nothing to undo. While the device is offline, these actions fail and aren't kept for later. How read state and unread counts change is on Read state and unread counts.
The local-only exception
Channel.markReadLocally() changes local state and sends nothing. It sets the current user's unread count in the channel to zero by dispatching a message.read_locally event, which is handled like a message.read event.
It's for busy channels such as livestreams, where read events are best turned off, so the server doesn't count unread messages. With read events off and channel.readEvents.localUnreadCountEnabled set in client.config (see Configuration), the client counts unread messages itself. Channel.markReadViaReporter() then marks the channel read locally, by calling Channel.markReadLocally(). Automatic marking read goes through it too, and so does the SDK's own marking read. A registered markReadRequest handler takes precedence: it runs instead.
Showing a pending state
A UI that should react at once shows a pending state of its own: from the call until the event arrives, or until the request fails. Here a menu item shows "Archiving..." until the membership changes:
const setArchiving = (archiving: boolean) => {
// Update your UI here, for example disable the menu item.
console.log(archiving ? "Archiving..." : "Done");
};
const archiveChannel = async () => {
setArchiving(true);
const stop = channel.state.subscribeWithSelector(
({ membership }) => ({ archivedAt: membership.archived_at }),
({ archivedAt }, previous) => {
// The first call reports the current value. Wait for the event to change it.
if (!previous || !archivedAt) return;
setArchiving(false);
stop();
},
);
try {
await channel.archive();
} catch (error) {
setArchiving(false);
stop();
throw error;
}
};Raw API calls
Some calls send the request and nothing else: Channel.sendMessage(), client.sendReaction() and Channel.sendReaction(), client.updateMessage() and client.deleteMessage(). They don't change local state, before or after the request:
// Nothing appears in the list until the server's message.new event arrives.
await channel.sendMessage({ message: { text: "Hello" } });In a watched channel, the server's events still reach the list: message.new, message.updated, message.deleted and the reaction events. But nothing is shown before them, and a failed request leaves no failed message to retry. Use them for scripts, or when you keep the local state yourself, and use channel.messageOperations or thread.messageOperations everywhere else.