Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

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.

Info:

Example

The user likes a message and 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.

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.

Used in