# Message composer

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

A message composer holds the message a user is writing: its text, attachments, poll, location and quoted message. It knows whether there is anything to send, and it turns what it holds into a message when the user sends. Every channel and every thread has one.

>
> **Info:** **Example**
>
> The user types "Hello @Bob" in a channel and adds a photo. The channel's composer keeps the text and the mention in its text composer, and the photo and its upload in its attachment manager. When the user presses send:
>
> 1. `channel.messageComposer.send()` runs the composition middleware, which builds the message twice: `localMessage`, the copy the message list shows at once, and `message`, the request the server receives.
> 2. The composer clears itself, before the request goes out, so anything the user types during the round trip is kept.
> 3. The channel's message operations add the message to the list as `sending`, send the request, and replace the message with the server's copy when it answers.
>

```ts
const result = await channel.messageComposer.send();

if (result === "nothing-to-send") {
  // The composer kept its content. A command or upload problem was also published
  // to client.notifications.
}
```

`MessageComposer.send()` and `MessageComposer.update()` never throw. They resolve to one of three results:

- `'sent'`: the request succeeded.
- `'nothing-to-send'`: there was nothing to compose, or a middleware stopped the composition, for example because a command isn't ready or an upload is still running. The composer keeps its content. When a command or a running upload stopped it, the reason has been [published as a notification](https://getstream.io/chat/docs/sdk/react/v15/state/publishing-notifications/).
- `'failed'`: the request failed. The composer was already cleared. The message stays in the list as `failed`, where the user can retry it, and an `api:message:send:failed` notification is published. For an edit, the notification is `api:message:update:failed`, and the edit is saved by editing again, not by retrying, as [Message operations](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/message-operations/#message-statuses) explains.

To keep the content after a send, set `retainCompositionOnSubmit` in the composer's [configuration](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/): a function that receives the composer and returns `true` to keep it. By default only a poll message keeps it: the poll goes out, and the text typed beside it stays.

Every method and store of the composer is listed in [MessageComposer API](https://getstream.io/chat/docs/sdk/react/v15/reference/state/messagecomposer-api/). How the message is sent and retried is on [Message operations](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/message-operations/).

## Composer kinds

| Composer | Where it comes from         | `contextType` | Sends to                                                              |
| -------- | --------------------------- | ------------- | --------------------------------------------------------------------- |
| Channel  | `channel.messageComposer`   | `'channel'`   | the channel                                                           |
| Thread   | `thread.messageComposer`    | `'thread'`    | the thread, as a reply                                                |
| Edit     | built from a message by you | `'message'`   | the message's thread when `client.threads` holds it, else the channel |

The channel and thread composers are created with their channel or thread and live as long as it does. A thread composer stamps every message with the thread's id as `parent_id`, and the reply lands in `thread.messagePaginator`.

An edit composer is one you build for a single message, when the edit shouldn't happen in the channel's or thread's own composer:

```ts
import { MessageComposer } from "stream-chat";

const editComposer = new MessageComposer({
  client,
  compositionContext: message,
  composition: message,
});
```

The message must carry its `cid`, which tells the composer which channel it belongs to. [Editing](#editing) shows the other way, editing in place.

Each composer has an `id`, which becomes the id of the message it sends. After a send, the composer takes a new one.

Configuration registered through `client.config` under `messageComposer` applies to every kind. To apply something to one kind only, check `composer.contextType` in a setup function, as [Composer middleware](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/composer-middleware/#adding-middleware) shows.

## Parts

A composer is made of parts. Each part keeps its data in a store of its own.

| Part                  | Holds                                                                          |
| --------------------- | ------------------------------------------------------------------------------ |
| `textComposer`        | the text, the cursor, mentions, the active command and the open suggestions    |
| `attachmentManager`   | attachments and their uploads                                                  |
| `linkPreviewsManager` | previews for the links in the text                                             |
| `pollComposer`        | the poll being created                                                         |
| `locationComposer`    | a location to share                                                            |
| `customDataManager`   | data of your own                                                               |
| the composer itself   | the quoted message, the poll id, "also send in channel" and the edited message |

Subscribe to a part's `state` to follow it. Getters such as `textComposer.text` or `composer.quotedMessage` return the value at the moment you read them, and don't tell you when it changes, so they suit event handlers, not rendering. See [Reactive state](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/reactive-state/).

```ts
const { attachmentManager } = channel.messageComposer;

const unsubscribe = attachmentManager.state.subscribeWithSelector(
  ({ attachments }) => ({ attachments }),
  ({ attachments }) => {
    const uploading = attachments.filter(
      (attachment) => attachment.localMetadata.uploadState === "uploading",
    );
    console.log(`${uploading.length} uploads running`);
  },
);
```

### Text

`TextComposer.handleChange({ text, selection })` is the input path. It runs the text middleware, which finds mentions and commands and opens suggestions for them, as [Suggestions](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/suggestions/) explains. `TextComposer.insertText({ text })` inserts at the cursor and goes the same way. `TextComposer.setText(text)` sets the text without running any middleware.

```ts
const { textComposer } = channel.messageComposer;

await textComposer.handleChange({
  selection: { end: 5, start: 5 },
  text: "Hello",
});
```

Two settings limit the length:

- `text.maxLengthOnEdit` cuts the text while the user types.
- `text.maxLengthOnSend` stops a longer message: `MessageComposer.send()` returns `'nothing-to-send'`, and no notification is published. A draft is cut to this length instead.

The channel type's `max_message_length` caps both. A composer that sets neither takes the server's limit.

While `text.publishTypingEvents` is on, which is the default, each text change reports a keystroke to the channel, which sends the typing events, and a send stops them. See [Typing indicators](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/typing-indicators/).

### Attachments and uploads

`AttachmentManager.uploadFiles(files)` adds files to the message and uploads them at once. Each attachment's `localMetadata.uploadState` tells where its upload is:

- `pending`: added, not uploading yet.
- `uploading`: on its way. `localMetadata.uploadProgress` goes from 0 to 100 while `attachments.trackUploadProgress` is on, which is the default.
- `finished`: uploaded, with its URL set.
- `failed`: the request failed, and an `api:attachment:upload:failed` notification was published.
- `blocked`: refused before uploading, because the app's upload settings don't allow its type or size. A `validation:attachment:upload:blocked` notification was published.

`attachmentManager.isUploadEnabled` says whether a file can be added now. Three things must hold: attachments are enabled, which also needs the channel type's `uploads` setting; a slot is free under `attachments.maxNumberOfFilesPerMessage` (10 by default); and the user has the `upload-file` capability. `attachments.fileUploadFilter` decides which files are taken. `attachments.acceptedFiles` lists the accepted file types for a file picker to offer; `stream-chat` doesn't check it.

To upload to your own storage, register an upload request. It resolves with the file's URL, and it can report progress and stop when the user removes the file. Set `customCdn: true` when the files never reach Stream, so that Stream's `uploads` setting and the `upload-file` capability no longer apply:

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

const uploadToMyStorage: UploadRequestFn = async (file, options) => {
  const body = new FormData();
  body.append("file", file as Blob);

  const response = await fetch("https://uploads.example.com/files", {
    body,
    method: "POST",
    signal: options?.abortSignal,
  });
  const { url } = await response.json();

  return { file: url };
};

client.config.set({
  messageComposer: {
    attachments: { customCdn: true, doUploadRequest: uploadToMyStorage },
  },
});
```

By default a message can't be sent while one of its uploads is running: `MessageComposer.send()` publishes a `validation:attachment:upload:in-progress` warning and returns `'nothing-to-send'`. With `attachments.pendingUploadsEnabled` on, the message appears in the list at once and its request waits for the uploads. If one of them fails, the message fails. See [Sending with pending uploads](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/sending-with-pending-uploads/).

### Link previews

When the text contains a URL, `linkPreviewsManager` waits for typing to pause (`linkPreviews.debounceURLEnrichmentMs`, 1500 ms by default) and loads a preview for it. Each preview has a status: `loading`, `loaded`, `failed`, `dismissed` or `pending`. `LinkPreviewsManager.dismissPreview(url)` removes one from the message.

Loaded previews are sent as attachments of the message, but while one of them is still loading, none is sent. Link previews are on by default and also need the channel type's `url_enrichment` setting. See [Link previews](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/link-previews/).

### Polls

`pollComposer` holds the poll form: `PollComposer.updateFields({ name: "Lunch?" })` changes fields and validates them, and `pollComposer.canCreatePoll` says whether the form is complete. Creating the poll takes two steps:

```ts
const composer = channel.messageComposer;

await composer.createPoll(); // creates the poll on the server and sets composer.pollId
await composer.send(); // sends the message that carries the poll
```

In a channel, the message carries only the poll. Text and attachments typed beside it stay in the composer. `MessageComposer.createPoll()` throws when the request fails, after publishing an `api:poll:create:failed` notification. Polls also need the channel type's `polls` setting. See [Polls](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/polls/).

### Location

`LocationComposer.setData({ latitude, longitude })` sets a location to share. Add `durationMs` to share a live location for that long; anything shorter than `location.minShareDurationMs` (one minute by default) isn't valid. `MessageComposer.sendLocation()` sends the location as a message of its own. In a thread it does nothing. When the request fails, it publishes an `api:location:create:failed` notification and throws.

Sharing a location also needs the channel type's `shared_locations` setting. See [Location sharing](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/location-sharing/).

### Custom data

Data of your own goes one of two ways:

- **Sent with the message.** Write it into `message.custom` with a composition middleware, and into `localMessage.custom` too, so the copy in the list shows it before the server answers. [Composer middleware](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/composer-middleware/#adding-middleware) has an example.
- **UI only.** `CustomDataManager.setCustomData()` keeps data in the composer's state, for example a flag your composer UI reads. It's never sent.

`CustomDataManager.setMessageData()` adds its fields to the top level of the outgoing message, not under `custom`, so it doesn't reach `message.custom`.

`customDataManager` starts empty when a draft is loaded or a message is edited: what it held isn't restored from them.

## Drafts

A draft keeps what a user wrote in a channel or a thread, on the server, until they come back. There is one draft per channel and one per thread. Drafts are off by default:

```ts
client.config.set({ messageComposer: { drafts: { enabled: true } } });
```

With drafts on:

- `MessageComposer.createDraft()` saves the composer's content as the draft. It runs the draft middleware, which skips the save when the composer is empty or nothing changed since the draft was loaded. The UI layer calls it when the composer goes away, for example when the user opens another channel.
- A channel query that returns a draft loads it into the channel's composer, and a thread created from server data starts with its draft. `MessageComposer.getDraft()` loads it on demand.
- While the composer's subscriptions run, emptying the composer deletes the draft, and `draft.updated` and `draft.deleted` events keep the composer in step with the user's other devices.

Drafts are skipped while a message is being edited, and for a channel that doesn't exist on the server yet. See [Drafts](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/drafts/).

## Editing

To edit a message in place, load it into a composer. This is how a channel's or thread's own composer switches to editing:

```ts
const composer = channel.messageComposer;

composer.initState({ composition: message });
// composer.editedMessage is now the message; text, attachments and mentions are loaded

await composer.textComposer.handleChange({
  selection: { end: 13, start: 13 },
  text: "Fixed a typo.",
});

const result = await composer.update();
```

Loading a message replaces what the composer held. `MessageComposer.update()` saves the edit and clears the composer, which leaves editing. The new text shows in the list at once. If the request fails, the edit stays on the message and the message is marked `failed`. `MessageComposer.clear()` leaves editing without saving, and `MessageComposer.restore()` puts the original message back into the composer.

While a message is being edited, commands can't be used and drafts aren't saved.

## Pitfalls

- **Saving an edit with `MessageComposer.send()`.** `MessageComposer.send()` always creates a message. On a composer holding an edit it reuses the message's id, so the server rejects the request as a duplicate and `MessageComposer.send()` returns `'failed'`, while the list already shows the edited text. Check `composer.editedMessage` and call `MessageComposer.update()`.
- **A server update during an edit.** A `message.updated` or reaction event for the message being edited reloads the composer from the server's copy, and unsaved changes are lost.
- **Upload settings on one composer.** Every upload in a channel goes through the channel composer's upload settings, including uploads from a thread or edit composer. Register `doUploadRequest` and `trackUploadProgress` for all composers through `client.config`, not on a single composer.
- **Reading getters while rendering.** Getters don't notify. Subscribe to the part's `state`.

## Used in

- [Sending, editing and deleting](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/sending-editing-and-deleting/)
- [Drafts](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/drafts/)
- [Quoted replies](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/quoted-replies/)
- [Attachments and uploads](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/attachments-and-uploads/)
- [Polls](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/polls/)
- [Location sharing](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/location-sharing/)
- [Link previews](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/link-previews/)
- [Message composer UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/message-composer-ui/), for the components that render a composer

---

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