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.

Info:

Example

The user likes a message, then archives the channel:

  1. The like appears on the message at once. channel.messageOperations.addReaction() changed the message before sending the request.
  2. The server answers, and its copy of the message, with its reaction counts, replaces the local one.
  3. The channel doesn't look archived yet. Channel.archive() only sends the request.
  4. The server sends a member.updated event. It updates the user's membership in channel.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.

Used in