# Composer middleware

> **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/).

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 type {
>   MessageComposerMiddlewareState,
>   MessageCompositionMiddleware,
>   MiddlewareHandlerParams,
> } from "stream-chat";
>
> declare module "stream-chat" {
>   interface CustomMessageData {
>     sent_from?: string;
>   }
> }
>
> 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: "web" },
>         },
>         message: {
>           ...state.message,
>           custom: { ...state.message.custom, sent_from: "web" },
>         },
>       }),
>   },
> };
>
> 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/v15/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()`.                                             |
| `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.                                        |
| `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/v15/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 app's upload settings refuse it. |
| `stream-io/attachment-manager-middleware/blocked-upload-notification` | Publishes a notification for a blocked file.                       |

`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 cancelled. |
| `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; put it in with `MiddlewareExecutor.replace()`.

`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.

- **When it runs.** A composer runs the setup function when it starts its subscriptions, which the UI layer does when it shows the composer. It runs it 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 type {
  MessageComposer,
  MessageComposerMiddlewareState,
  MessageCompositionMiddleware,
  MiddlewareHandlerParams,
} from "stream-chat";

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 table 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 composer's content, empties it and keeps the command. `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.

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

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) 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 middlewares

When a user picks a command such as `/giphy`, the composer keeps it in `textComposer.command` and takes it out of the text. A UI can show it apart from the text, for example as a chip. Four middlewares, which `stream-chat` exports but doesn't install, 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`. Install all four, in the same setup function as the rest of your composer setup:

```ts
import type { TextComposerMiddleware } from "stream-chat";
import {
  createActiveCommandGuardMiddleware,
  createCommandInjectionMiddleware,
  createCommandStringExtractionMiddleware,
  createDraftCommandInjectionMiddleware,
} from "stream-chat";

client.config.setSetupFunction("messageComposer", ({ composer }) => {
  composer.compositionMiddlewareExecutor.insert({
    middleware: [createCommandInjectionMiddleware(composer)],
    position: { after: "stream-io/message-composer-middleware/attachments" },
    unique: true,
  });
  composer.draftCompositionMiddlewareExecutor.insert({
    middleware: [createDraftCommandInjectionMiddleware(composer)],
    position: {
      after: "stream-io/message-composer-middleware/draft-attachments",
    },
    unique: true,
  });
  composer.textComposer.middlewareExecutor.insert({
    middleware: [
      createActiveCommandGuardMiddleware() as TextComposerMiddleware,
    ],
    position: { before: "stream-io/text-composer/commands-middleware" },
    unique: true,
  });
  composer.textComposer.middlewareExecutor.insert({
    middleware: [
      createCommandStringExtractionMiddleware() as TextComposerMiddleware,
    ],
    position: { after: "stream-io/text-composer/commands-middleware" },
    unique: true,
  });

  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 guard sits after the mentions middleware, so mentions keep working while a command is active, which commands such as `/ban @ann` need.

## Used in

- [Mentions](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/mentions/)
- [Commands and Giphy](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/commands-and-giphy/)
- [Emoji](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/emoji/)
- [Attachments and uploads](https://getstream.io/chat/docs/sdk/react/v15/features/rich-content/attachments-and-uploads/)
- [Attachment actions](https://getstream.io/chat/docs/sdk/react/v15/recipes/composer/attachment-actions/)

---

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