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.
Example
The user likes a message and 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.
Message operations
These actions change local state before the request goes out:
| Action | Shown at once | When the request fails |
|---|---|---|
MessageOperations.send() and MessageOperations.retry() |
the message, with the status sending |
the message stays, marked failed, ready to retry |
MessageOperations.update() |
the new text, marked as edited | the edit stays, and the message is marked failed; editing again saves it, retrying doesn't |
MessageOperations.delete() |
the message as deleted, or gone for a hard delete | the message comes back |
MessageOperations.addReaction() and MessageOperations.deleteReaction() |
the reaction added or removed | undone |
Poll.castVote() and Poll.removeVote() |
the vote cast or removed | undone |
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 |
The composer's MessageComposer.send() and MessageComposer.update() go through these operations, so they behave the same way. The composer also clears itself before the request goes out.
When the server answers, its copy replaces the local one. For a send or an edit, that happens only if no newer copy arrived in the meantime, for example through a message.updated event. 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.
With offline support, an edit, a delete or a reaction that waits in the offline queue 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.
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. 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 readEvents.localUnreadCountEnabled in the channel's configuration, the client counts them itself. Marking such a channel read with Channel.markReadViaReporter(), which automatic marking read uses too, then calls Channel.markReadLocally() for you, unless a markReadRequest handler is registered.
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.