# Message operations

Message operations cover the step between the composer and the server: how a message's request is sent, shown in the list, and settled. They're the interface for sending, retrying, editing and deleting messages, and for adding or removing reactions.

This page explains how to:

- follow a message's request: whether it's still sending, saved by the server, or failed;
- rely on optimistic updates: what the list shows at once for each operation, and what's undone when its request fails;
- call the operations the composer doesn't cover, such as retrying a failed send or deleting a message;
- handle the operations' requests your own way, by registering request handlers, for example to send them through your backend.

>
> **Info:** **Example**
>
> 1. The user sends "Hello". The message appears in the list at once, as sending, due to optimistic update.
> 2. The request fails. The message stays in the list, marked failed, with the server's error.
> 3. The user retries. The message is marked as sending again, and the same request goes out.
> 4. The server answers. Its copy replaces the local one, and the message is marked as received.
>

```ts
const retryMessage = async (messageId: string) => {
  const localMessage = channel.messagePaginator.getItem(messageId);
  if (localMessage?.status !== "failed") return;

  try {
    await channel.messageOperations.retry({ localMessage });
  } catch {
    // The message is marked failed again, ready for another retry.
  }
};
```

Unlike the composer's `MessageComposer.send()`, the operations reject when the request fails. The list is already settled by then, so the rejection only tells the caller.

## Operations

Every channel has its operations interface in `channel.messageOperations`, and every thread in `thread.messageOperations`. The `MessageOperations` class itself isn't exported from `stream-chat`. A thread's operations send to the thread's channel, with the thread's id as `parent_id`, and the reply lands in `thread.messagePaginator`. The composer's `MessageComposer.send()` and `MessageComposer.update()` use them underneath.

| Operation                                                          | Takes                                                                    |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| `MessageOperations.send({ localMessage, message?, options? })`     | The message to show, and optionally the request body and send options.   |
| `MessageOperations.retry({ localMessage, message?, options? })`    | A failed message. Without `message`, it resends the request that failed. |
| `MessageOperations.update({ localMessage, options? })`             | The edited message. `options` takes `skip_enrich_url` and `skip_push`.   |
| `MessageOperations.delete({ localMessage, options? })`             | The message. `options` takes `hard` and `delete_for_me`.                 |
| `MessageOperations.addReaction({ messageId, reaction, options? })` | The message id and the reaction, such as `{ type: "like" }`.             |
| `MessageOperations.deleteReaction({ messageId, type })`            | The message id and the reaction type.                                    |

When a change should show in the message list, use the operations, not raw API calls such as `Channel.sendMessage()` or `client.updateMessage()`. A raw call only sends the request: in a watched channel, the list changes when the server's event arrives, and a failed request isn't shown in it. [Raw API calls](https://getstream.io/chat/docs/sdk/react/state/writing-messages/whats-optimistic-and-what-isnt/#raw-api-calls) lists them.

## Message statuses

A message on its way has one of three statuses in `message.status`:

| `message.status` | Means                                                      | What your UI can do                                                                                                                                                                                                                             |
| ---------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sending`        | The message is in the list, and the request is on its way. | Show it as pending.                                                                                                                                                                                                                             |
| `received`       | The server's copy is in the list.                          | Show it as sent. Whether it was delivered and read comes from `channel.messageReceiptsTracker`: see [Who received and read a message](https://getstream.io/chat/docs/sdk/react/features/messaging/read-state-unread-and-receipts/#who-received-and-read-a-message). |
| `failed`         | The request failed, and `message.error` says why.          | Offer a retry for a send, with `MessageOperations.retry({ localMessage })`. A failed edit is saved by editing again, with `MessageOperations.update()`, not by retrying.                                                                        |

These statuses describe the request, and don't match the statuses a UI shows on the user's own messages: sending, sent, delivered and read. A UI shows a `received` message as sent until another member has it, and takes delivered and read from the other members' read state, as [Who received and read a message](https://getstream.io/chat/docs/sdk/react/features/messaging/read-state-unread-and-receipts/#who-received-and-read-a-message) explains.

A failed send keeps its request for five minutes, for up to 100 messages, so a retry sends exactly what failed. After that, a retry rebuilds the request from the message. Both limits can be changed under `messageOperations` in [Configuration](https://getstream.io/chat/docs/sdk/react/state/state-and-entities/configuration/).

If the server rejects a send because the message already exists, the earlier attempt did arrive. The message is marked `received` rather than `failed`, although the call still rejects.

## Optimistic updates

The operations change the list before the request goes out:

| Operation                                                                  | Shown at once                                        | When the request fails    |
| -------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------- |
| `MessageOperations.send()` and `MessageOperations.retry()`                 | the message, as `sending`                            | marked `failed`           |
| `MessageOperations.update()`                                               | the new text, marked as edited                       | kept, and marked `failed` |
| `MessageOperations.delete()`                                               | the message as deleted, or removed for a hard delete | undone                    |
| `MessageOperations.addReaction()` and `MessageOperations.deleteReaction()` | the reaction appears or disappears                   | undone                    |

When the server answers, its copy replaces the local one. For a send or an edit, that happens only if nothing newer arrived in the meantime, such as a `message.updated` event.

Conversation actions, such as marking a channel read or archiving it, work the other way: their effect appears when the server's event arrives. Every action is listed on [What's optimistic and what isn't](https://getstream.io/chat/docs/sdk/react/state/writing-messages/whats-optimistic-and-what-isnt/).

## Customizing requests

To change how a request is made, register a request handler in `client.config`. The operation then calls your handler instead of making the request itself. It passes the handler the default request, the one it would make without a handler, as the second argument. Call it to send the usual way, with the parameters as given or changed, for example with `skip_push: true` in `options`. Leave it out to send somewhere else:

```ts
import type { OperationRequestFn } from "stream-chat";

const sendMessageRequest: OperationRequestFn<"send"> = async (
  params,
  defaultRequest,
) => {
  if (!params.localMessage.cid.startsWith("support:")) {
    return defaultRequest(params);
  }

  const response = await fetch("https://api.example.com/support/messages", {
    body: JSON.stringify(params.message),
    headers: { "Content-Type": "application/json" },
    method: "POST",
  });
  const { message } = await response.json();

  return { message };
};

client.config.set({ channel: { requestHandlers: { sendMessageRequest } } });
```

Here, messages in `support` channels go through the app's backend, which sends them to Stream and returns the server's copy. Every other channel sends the usual way.

The `channel` key in `client.config` names the kind of object the handlers belong to, not one channel: the handlers apply to every channel, the ones that exist and the ones created later. A channel has no settings of its own (see [Settings for one object](https://getstream.io/chat/docs/sdk/react/state/state-and-entities/configuration/#settings-for-one-object)), so to treat channels differently, branch inside the handler, for example on `params.localMessage.cid`. There's no request function for a single call: every call goes through the registered handler.

The optimistic update happens before the handler runs, and settling the list happens after it, whatever the handler does.

### Request handlers

| Handler key in config     | Runs on                                                                                   | Receives                             | Resolves to   |
| ------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------ | ------------- |
| `sendMessageRequest`      | `MessageOperations.send()`, and `MessageOperations.retry()` when there's no retry handler | `{ localMessage, message, options }` | `{ message }` |
| `retrySendMessageRequest` | `MessageOperations.retry()`                                                               | `{ localMessage, message, options }` | `{ message }` |
| `updateMessageRequest`    | `MessageOperations.update()`                                                              | `{ localMessage, options }`          | `{ message }` |
| `deleteMessageRequest`    | `MessageOperations.delete()`                                                              | `{ localMessage, options }`          | `{ message }` |

In `send` and `retry`, `message` is the request body and `localMessage` the copy in the list. The message the handler resolves with is the server's copy, and it replaces the local one.

Marking a channel or a thread read has a request handler too, `markReadRequest`. [Read state and unread counts](https://getstream.io/chat/docs/sdk/react/state/channels-and-threads/read-state-and-unread-counts/#marking-read-and-unread) explains it.

### Threads use their channel's handlers

A thread reply is sent, retried, edited and deleted through the handlers registered under `channel`. There are no message handlers under `thread`.

## Pitfalls

- **Resolve with the server's message.** A send handler that resolves without a `message` leaves the local copy as `sending` until the server's `message.new` event replaces it.
- **Retrying a failed edit.** `MessageOperations.retry()` sends the edited message as a new one. The server rejects it because the message exists, and the list then shows the edit as `received`, although the server kept the old text. Load the message into a composer and call `MessageComposer.update()` again.

## Used in

- [Sending, editing and deleting](https://getstream.io/chat/docs/sdk/react/features/messaging/sending-editing-and-deleting/)
- [Reactions](https://getstream.io/chat/docs/sdk/react/features/messaging/reactions/)
- [Message actions](https://getstream.io/chat/docs/sdk/react/features/messaging/message-actions/)
- [Sending with pending uploads](https://getstream.io/chat/docs/sdk/react/features/messaging/sending-with-pending-uploads/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/state/writing-messages/message-operations/](https://getstream.io/chat/docs/sdk/react/state/writing-messages/message-operations/).