# Composer middleware

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

Composer middleware changes what a message composer does at each step: when the text changes, when a suggestion is picked, before and after a file uploads, and when the message is built. Each step runs a chain of small handlers, and you can add your own, move them or replace the built-in ones.

>
> **Info:** **Example**
>
> An app wants every message sent from a channel to carry the platform it was sent from, in `message.custom.sent_from`. A middleware on the composition chain adds it while the message is built:
>
> ```ts
> import { Platform } from "react-native";
> import { StreamChat } from "stream-chat";
> import type {
>   MessageComposerMiddlewareState,
>   MessageCompositionMiddleware,
>   MiddlewareHandlerParams,
> } from "stream-chat";
>
> declare module "stream-chat" {
>   interface CustomMessageData {
>     sent_from?: string;
>   }
> }
>
> const client = StreamChat.getInstance("YOUR_API_KEY");
>
> const sentFromMiddleware: MessageCompositionMiddleware = {
>   id: "my-app/sent-from",
>   handlers: {
>     compose: ({
>       next,
>       state,
>     }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) =>
>       next({
>         ...state,
>         localMessage: {
>           ...state.localMessage,
>           custom: { ...state.localMessage.custom, sent_from: Platform.OS },
>         },
>         message: {
>           ...state.message,
>           custom: { ...state.message.custom, sent_from: Platform.OS },
>         },
>       }),
>   },
> };
>
> client.config.setSetupFunction("messageComposer", ({ composer }) => {
>   if (composer.contextType !== "channel") return;
>
>   composer.compositionMiddlewareExecutor.insert({
>     middleware: [sentFromMiddleware],
>     position: {
>       before: "stream-io/message-composer-middleware/data-validation",
>     },
>     unique: true,
>   });
>
>   return () =>
>     composer.compositionMiddlewareExecutor.remove(sentFromMiddleware.id);
> });
> ```
>

The middleware changes both copies of the message: `message`, which the server receives, and `localMessage`, which the message list shows until the server answers. Custom fields go under `custom`, and the `declare module` block types `sent_from` there, as [Data conventions](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/data-conventions/) explains. The setup function installs it on every channel composer, and its teardown removes it again.

## How a chain runs

A chain is a list of middleware, each an object with an `id` and a handler per event. When the composer runs an event, it calls the handlers for that event in order. A middleware without a handler for the event is skipped.

Each handler receives the current `state` and four functions. It returns the result of calling one of them:

- `next(state)` passes a changed state to the next middleware.
- `forward()` passes the state on unchanged.
- `complete(state)` stops the chain. The state it's given is the result.
- `discard()` stops the chain and drops the result. For a message, nothing is sent and `MessageComposer.send()` returns `'nothing-to-send'`. For a text change, the change isn't applied.

>
> **Info:** **Example**
>
> When the user types "Hi @an", the text chain runs `onChange`. The pre-validation middleware forwards, the mentions middleware finds the `@` trigger and completes with suggestions for "an", and the commands middleware never runs for that change.
>

A handler can be `async`. When the same chain starts again before an earlier run finished, for example on the next keystroke, the earlier run's result is dropped. The upload chains are the exception: they run once per file, side by side, and keep every result.

## Built-in chains

Every composer starts with the chains below. Each table lists the middleware in the order it runs.

### Composition

`composer.compositionMiddlewareExecutor` runs `compose` when `MessageComposer.send()` or `MessageComposer.update()` builds the message. Its state is `{ message, localMessage, sendOptions }`.

| Middleware id                                               | What it does                                                                                                    |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `stream-io/message-composer-middleware/user-data-injection` | Adds the current user to `localMessage`.                                                                        |
| `stream-io/message-composer-middleware/poll-only`           | In a channel composer that holds a poll, completes the chain with a message that carries only the poll.         |
| `stream-io/message-composer-middleware/text-composition`    | Adds the text, and the mentions still present in it.                                                            |
| `stream-io/message-composer-middleware/attachments`         | Adds the finished uploads. Discards the message while an upload is running, unless pending uploads are allowed. |
| `stream-io/message-composer-middleware/link-previews`       | Adds the loaded link previews as attachments, unless one of them is still loading.                              |
| `stream-io/message-composer-middleware/shared-location`     | Adds the location to share.                                                                                     |
| `stream-io/message-composer-middleware/own-state`           | Adds the quoted message, the poll and "also send in channel".                                                   |
| `stream-io/message-composer-middleware/custom-data`         | Adds the custom data set with `CustomDataManager.setMessageData()` under `custom`.                              |
| `stream-io/message-composer-middleware/data-validation`     | Discards an empty message, one longer than `text.maxLengthOnSend`, and one whose command can't be sent.         |
| `stream-io/message-composer-middleware/data-cleanup`        | Merges in the message being edited, if there is one, and sets the message type.                                 |

### Drafts

`composer.draftCompositionMiddlewareExecutor` runs `compose` when `MessageComposer.createDraft()` builds the draft. Its state is `{ draft }`.

| Middleware id                                                  | What it does                                                         |
| -------------------------------------------------------------- | -------------------------------------------------------------------- |
| `stream-io/message-composer-middleware/draft-text-composition` | Adds the text, cut to `text.maxLengthOnSend`, and its mentions.      |
| `stream-io/message-composer-middleware/draft-attachments`      | Adds the finished uploads. Running uploads don't stop the draft.     |
| `stream-io/message-composer-middleware/draft-link-previews`    | Adds the loaded link previews.                                       |
| `stream-io/message-composer-middleware/draft-own-state`        | Adds the quoted message, the poll and "also send in channel".        |
| `stream-io/message-composer-middleware/draft-custom-data`      | Adds the custom message data under `custom`.                         |
| `stream-io/message-composer-middleware/draft-data-validation`  | Discards a draft with no content, or with no change since it loaded. |

### Text

`composer.textComposer.middlewareExecutor` runs `onChange` on every text change and `onSuggestionItemSelect` when a suggestion is picked. Its state is the text composer's state, plus `change.selectedSuggestion` for a picked suggestion and `effects` to apply afterwards.

| Middleware id                                        | What it does                                                                                                                                               |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `stream-io/text-composer/pre-validation-middleware`  | Cuts the text to `text.maxLengthOnEdit`.                                                                                                                   |
| `stream-io/text-composer/mentions-middleware`        | Opens mention suggestions after `@`, and inserts a picked mention. See [Suggestions](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/suggestions/). |
| `stream-io/text-composer/commands-middleware`        | Opens command suggestions after `/`, and sets a picked or typed command.                                                                                   |
| `stream-io/text-composer/command-effects-middleware` | Adds the `command.activate` effect when a command is picked. See [Effect handlers](#effect-handlers).                                                      |

### Uploads

`attachmentManager.preUploadMiddlewareExecutor` runs `prepare` for each added file, with the state `{ attachment }`. A `discard()` leaves the file out.

| Middleware id                                                         | What it does                                                                                                                             |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `stream-io/attachment-manager-middleware/file-upload-config-check`    | Marks the file `blocked` when the Stream app's upload settings, set in the Dashboard, refuse its type or size.                           |
| `stream-io/message-composer-ui-middleware/video-attachment-preview`   | Added by the SDK. Sets a video's `localMetadata.previewUri` to its thumbnail, because the video file itself can't be shown as a preview. |
| `stream-io/attachment-manager-middleware/blocked-upload-notification` | Publishes a notification for a blocked file.                                                                                             |

The SDK inserts the video preview middleware into the composer the `MessageComposer` component shows.

`attachmentManager.postUploadMiddlewareExecutor` runs `postProcess` after each upload, with the state `{ attachment, error, response }`. A `discard()` removes the attachment.

| Middleware id                                                    | What it does                                                          |
| ---------------------------------------------------------------- | --------------------------------------------------------------------- |
| `stream-io/attachment-manager-middleware/upload-error`           | Publishes a notification for a failed upload, unless it was canceled. |
| `stream-io/attachment-manager-middleware/post-upload-enrichment` | Sets the attachment's URL and thumbnail from the upload response.     |

### Polls

`pollComposer.stateMiddlewareExecutor` runs `handleFieldChange` and `handleFieldBlur` with `stream-io/poll-composer-state-processing`, which processes and validates the poll's fields. `createPollComposerStateMiddleware({ processors, validators })` builds that middleware with processors and validators of your own. See [Poll validators and processors](#poll-validators-and-processors).

`pollComposer.compositionMiddlewareExecutor` runs `compose` with `stream-io/poll-composer-composition`, which discards a poll that can't be created yet.

## Adding middleware

Add middleware in the composer setup function, as in the example at the top. `client.config.setSetupFunction("messageComposer", setup)` runs `setup` for every composer: channel, thread and edit composers, the ones that exist and the ones created later. Register it once, right after creating the client.

- **When it runs.** A composer runs the setup function when it starts its subscriptions. The SDK starts them for the composer `<Channel>` or `<Thread>` shows, and for the edit composer while the user edits a message. It runs the setup again whenever the setup function or the `messageComposer` configuration changes.
- **The teardown.** The function the setup returns runs before the setup runs again, and when the composer's subscriptions end. Remove there whatever the setup inserted.
- **`unique: true`.** An `MiddlewareExecutor.insert()` with `unique: true` first removes a middleware with the same id, so running the setup twice can't install two copies.
- **Which composers.** Check `composer.contextType`, which is `'channel'`, `'thread'` or `'message'` (an edit composer), to install middleware on some composers only.

A key has one setup function. Registering another one replaces the first, after running its teardown. Put all your composer setup in one function. `client.setMessageComposerSetupFunction(setup)` is deprecated and registers the same function; use `client.config.setSetupFunction()`.

To change the composer's data in a middleware, build the middleware per composer:

```ts
import { StreamChat } from "stream-chat";
import type {
  MessageComposer,
  MessageComposerMiddlewareState,
  MessageCompositionMiddleware,
  MiddlewareHandlerParams,
} from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

const createSignatureMiddleware = (
  composer: MessageComposer,
): MessageCompositionMiddleware => ({
  id: "my-app/signature",
  handlers: {
    compose: ({
      forward,
      next,
      state,
    }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
      if (composer.editedMessage || !state.message.text) return forward();

      const text = `${state.message.text}\n\nSent from the support desk`;
      return next({
        ...state,
        localMessage: { ...state.localMessage, text },
        message: { ...state.message, text },
      });
    },
  },
});

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  const signature = createSignatureMiddleware(composer);

  composer.compositionMiddlewareExecutor.insert({
    middleware: [signature],
    position: {
      after: "stream-io/message-composer-middleware/text-composition",
    },
    unique: true,
  });

  return () => composer.compositionMiddlewareExecutor.remove(signature.id);
});
```

## Ordering and replacing middleware

Every chain has the same methods:

| Method                                                        | What it does                                                                                       |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `MiddlewareExecutor.use(middleware)`                          | Adds one middleware or a list at the end.                                                          |
| `MiddlewareExecutor.insert({ middleware, position, unique })` | Adds a list before or after another middleware: `position` is `{ before: id }` or `{ after: id }`. |
| `MiddlewareExecutor.replace(middleware)`                      | Puts each middleware in the place of the one with the same id. One with a new id goes at the end.  |
| `MiddlewareExecutor.remove(id)`                               | Removes the middleware with that id, or with any id in a list.                                     |
| `MiddlewareExecutor.setOrder(ids)`                            | Rebuilds the chain from the listed ids, in that order.                                             |
| `installedMiddleware`                                         | The chain as it is now, in order.                                                                  |

Two of them fail quietly.

- **An unknown target id.** `MiddlewareExecutor.insert()` doesn't check that the id in `position` exists. With `after`, the middleware lands at the start of the chain. With `before`, it lands just before the last middleware. Copy ids from the tables above, or check `installedMiddleware` first.
- **`MiddlewareExecutor.setOrder()` drops what you don't list.** The chain keeps only the ids you pass, so leaving out a built-in id removes that middleware.

```ts
const executor = channel.messageComposer.textComposer.middlewareExecutor;

const hasMentions = executor.installedMiddleware.some(
  ({ id }) => id === "stream-io/text-composer/mentions-middleware",
);
```

## Pitfalls

- **Change both `message` and `localMessage`.** `message` is what the server receives, and `localMessage` is what the list shows until the server's copy replaces it. Change only `message`, and the list shows the old content until the server answers. Change only `localMessage`, and the change never reaches the server and disappears when its answer arrives.
- **Poll messages skip most of the chain.** In a channel, `poll-only` completes the chain for a message that carries a poll, so middleware after it doesn't run for that message.
- **Return what `next`, `forward`, `complete` or `discard` returns.** A handler that returns nothing breaks the chain, and calling `next` twice throws.
- **Don't change `state` in place.** Spread it into a new object, as the examples do.
- **One setup function per key.** A second `client.config.setSetupFunction("messageComposer", ...)` replaces the first.

## Effect handlers

Some changes have to wait until a chain has finished. A text middleware can't apply them itself, because a later middleware may still discard the change. Instead, it adds an effect to `state.effects`, and the composer applies the effects after the text chain has committed its result.

The composer has two built-in effects. `command.activate` runs when a command is picked. It saves the attachments, the poll and the rest of the composer's content, cancels the uploads still running, empties the composer and keeps the command. For a command picked from the suggestions, the saved content has an empty text. `command.clear` runs on `TextComposer.clearCommand()` and restores what was saved.

Register a handler for an effect type of your own with `MessageComposer.registerEffectHandler(type, handler)`, and add effects of that type in a text middleware. Registering a handler for a built-in type replaces the built-in one.

The `@` list holds users, `@channel`, `@here`, roles and user groups, so this middleware checks `mentionType` and only reports picked users:

```ts
import { StreamChat } from "stream-chat";
import type {
  MiddlewareHandlerParams,
  TextComposerMiddleware,
  TextComposerMiddlewareExecutorState,
} from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

type MentionPickedEffect = { type: "my-app/mention.picked"; userId: string };

const mentionEffectsMiddleware: TextComposerMiddleware = {
  id: "my-app/mention-effects",
  handlers: {
    onChange: ({ forward }) => forward(),
    onSuggestionItemSelect: ({
      forward,
      next,
      state,
    }: MiddlewareHandlerParams<TextComposerMiddlewareExecutorState>) => {
      const selected = state.change?.selectedSuggestion;
      if (
        state.suggestions?.trigger !== "@" ||
        !selected ||
        !("mentionType" in selected) ||
        selected.mentionType !== "user"
      ) {
        return forward();
      }

      const effect: MentionPickedEffect = {
        type: "my-app/mention.picked",
        userId: selected.id,
      };
      return next({ ...state, effects: [...(state.effects ?? []), effect] });
    },
  },
};

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  composer.registerEffectHandler<MentionPickedEffect>(
    "my-app/mention.picked",
    (effect) => console.log(`Mentioned ${effect.userId}`),
  );

  // Before the mentions middleware, which completes the chain when a mention is picked.
  composer.textComposer.middlewareExecutor.insert({
    middleware: [mentionEffectsMiddleware],
    position: { before: "stream-io/text-composer/mentions-middleware" },
    unique: true,
  });

  return () =>
    composer.textComposer.middlewareExecutor.remove(
      mentionEffectsMiddleware.id,
    );
});
```

The effect runs only when the pick is committed. If a later middleware discarded it, the handler wouldn't run.

## Command middleware

When a user picks a command such as `/giphy`, the composer keeps it in `textComposer.command` and takes it out of the text. The SDK's composer shows the active command as a chip next to the text input, and the user types the command's arguments in the input. Four middleware, which `stream-chat` exports and neither `stream-chat` nor the SDK installs, make that work end to end:

| Factory                                           | Middleware id                                                          | What it does                                                                |
| ------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `createActiveCommandGuardMiddleware()`            | `stream-io/text-composer/active-command-guard`                         | While a command is active, stops the text chain before command suggestions. |
| `createCommandStringExtractionMiddleware()`       | `stream-io/text-composer/command-string-extraction`                    | Takes a typed command out of the text: `/ban @ann spam` keeps `@ann spam`.  |
| `createCommandInjectionMiddleware(composer)`      | `stream-io/message-composer-middleware/command-string-injection`       | Puts the command back in front of the text when the message is sent.        |
| `createDraftCommandInjectionMiddleware(composer)` | `stream-io/message-composer-middleware/draft-command-string-injection` | Does the same when a draft is saved.                                        |

Without them, a picked command leaves the text and doesn't come back: picking `/giphy` and typing `cats` sends plain `cats`. `setupCommandUIMiddlewares(composer)`, from the SDK, inserts all four. Call it in the same setup function as the rest of your composer setup, and remove the four in the teardown:

```ts
import { StreamChat } from "stream-chat";
import { setupCommandUIMiddlewares } from "stream-chat-react-native";

const client = StreamChat.getInstance("YOUR_API_KEY");

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  setupCommandUIMiddlewares(composer);

  return () => {
    composer.compositionMiddlewareExecutor.remove(
      "stream-io/message-composer-middleware/command-string-injection",
    );
    composer.draftCompositionMiddlewareExecutor.remove(
      "stream-io/message-composer-middleware/draft-command-string-injection",
    );
    composer.textComposer.middlewareExecutor.remove([
      "stream-io/text-composer/active-command-guard",
      "stream-io/text-composer/command-string-extraction",
    ]);
  };
});
```

The teardown matters here. `setupCommandUIMiddlewares()` inserts only the first of the four with `unique: true`, so a setup that runs again without the teardown installs the other three twice.

The guard sits after the mentions middleware, so mentions keep working while a command is active, which commands such as `/ban @ann` need.

## Poll validators and processors

`PollComposer.updateFields()` runs `handleFieldChange` on the poll state chain, with the fields it changes. `PollComposer.handleFieldBlur(field)` runs `handleFieldBlur` with one field. The SDK's creation sheet calls it when the user leaves the question, an option or the maximum number of votes.

For each field in the change, the built-in middleware runs the field's processor, then the field's validator:

- A processor receives `{ data, value }` and returns the fields to store, such as `{ name: value.trim() }`. It can return more than one field.
- A validator receives `{ data, value, currentError }` and returns errors by field. `{ name: undefined }` clears the question's error. It can set or clear the errors of other fields too.

`data` is the poll before the change, `value` is the field's new value after its processor, and `currentError` is the field's error before the change. The built-in functions are exported:

| Event               | Processors                                                                                                                                                                                                                                                       | Validators                                                                                                                                                                                               |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `handleFieldChange` | `pollCompositionStateProcessors`. They keep `max_votes_allowed` between 2 and 10, empty it when `enforce_unique_vote` changes, remove an option the user empties unless it's the last, and add an empty option at the end when none is empty, up to 100 options. | `pollStateChangeValidators` and `defaultPollFieldChangeEventValidators`. They flag duplicate options and an invalid `max_votes_allowed`, and clear the question's error once the user types.             |
| `handleFieldBlur`   | None.                                                                                                                                                                                                                                                            | `pollStateChangeValidators` and `defaultPollFieldBlurEventValidators`. They flag duplicate options and an invalid `max_votes_allowed`, require a question, and flag an empty option other than the last. |

`createPollComposerStateMiddleware({ processors, validators })` takes your functions by event, then by field, as in `{ handleFieldBlur: { name: validateName } }`. Each one replaces the built-in function for the same field and event, and the other built-in functions keep running. To keep a built-in check, call the built-in function from yours. A processor of your own for `options` replaces the one that removes and adds options. Its `value` is the whole list when the options are reordered, and `{ index, text }` when one option changes. `isTargetedOptionTextUpdate(value)` tells them apart.

An error is a `PollComposerValidationError`, `{ code, message, metadata? }`. `stream-chat` builds its own with `pollComposerValidationError(code)`, and the type of `code` allows only the codes in `POLL_COMPOSER_VALIDATION_CODE`. For a check of your own, give the error a code of your own, cast to `PollComposerValidationCode`, and the text to show as `message`. A prefix such as `my-app:` keeps your codes apart from the built-in ones.

The SDK's creation sheet shows an option's error under the option. For the built-in `optionDuplicate` and `optionEmpty` codes it shows its own translated text, and for any other code the error's `message`. It shows no text for an error of the question or of `max_votes_allowed`. Any error disables the send button, because `PollComposer.canCreatePoll` is `false` while an error is set.

This setup limits each option to 40 characters, and keeps the built-in checks for duplicate and empty options:

```ts
import {
  createPollComposerStateMiddleware,
  defaultPollFieldBlurEventValidators,
  pollStateChangeValidators,
  StreamChat,
} from "stream-chat";
import type {
  PollComposerOption,
  PollComposerValidationCode,
  PollComposerValidationError,
  PollStateChangeValidator,
} from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

const MAX_OPTION_LENGTH = 40;

// The type of `code` allows only the built-in codes, so a code of your own is cast.
const ownError = (
  code: string,
  message: string,
): PollComposerValidationError => ({
  code: code as PollComposerValidationCode,
  message,
});

const withLengthCheck =
  (builtIn: PollStateChangeValidator | undefined): PollStateChangeValidator =>
  (params) => {
    const errors = { ...builtIn?.(params).options };
    const options: PollComposerOption[] = params.value;

    options.forEach((option) => {
      if (!errors[option.id] && option.text.length > MAX_OPTION_LENGTH) {
        errors[option.id] = ownError(
          "my-app:poll:option:tooLong",
          `Use at most ${MAX_OPTION_LENGTH} characters`,
        );
      }
    });

    return { options: Object.keys(errors).length > 0 ? errors : undefined };
  };

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  const { stateMiddlewareExecutor } = composer.pollComposer;

  stateMiddlewareExecutor.replace([
    createPollComposerStateMiddleware({
      validators: {
        handleFieldChange: {
          options: withLengthCheck(pollStateChangeValidators.options),
        },
        handleFieldBlur: {
          options: withLengthCheck(defaultPollFieldBlurEventValidators.options),
        },
      },
    }),
  ]);

  return () =>
    stateMiddlewareExecutor.replace([createPollComposerStateMiddleware()]);
});
```

`MiddlewareExecutor.replace()` puts the new middleware in the place of the built-in one, because both have the same id, and the teardown puts a default one back.

## Used in

- [Mentions](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/mentions/)
- [Commands and Giphy](https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/commands-and-giphy/)
- [Emoji](https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/emoji/)
- [Attachments and uploads](https://getstream.io/chat/docs/sdk/react-native/v10/features/rich-content/attachments-and-uploads/)
- [Commands UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/commands-ui/)

---

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