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, before the request goes out.
- The server answers, and its copy of the message, with the reaction counts, replaces the local one.
- The channel doesn't look archived yet. Archiving only sends the request.
- The server's event arrives. The user's membership is updated, 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.
These actions are optimistic too:
| Action | Shown at once | When the request fails |
|---|---|---|
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 |
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.
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 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. 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.