# MessageComposer Middleware

## Message Composer Middleware

MessageComposer middleware lets you control data processing across composition scenarios.

## Best Practices

- Keep middleware small and focused to avoid side effects.
- Use `unique: true` or stable middleware IDs to prevent duplicates.
- Insert middleware at explicit positions to control ordering.
- Avoid heavy work in middleware to keep typing responsive.
- Scope middleware behavior by context type when needed.

Some managers (`TextComposer`, `PollComposer`, `MessageComposer`) use middleware to process state changes and compose data. Each has one or more middleware executors that run handler chains.

The middleware execution is performed via `MiddlewareExecutor`. It allows:

- Registering middleware with specific handlers
- Controlling handler execution order
- Deciding when to terminate the middleware chain

### Middleware Execution

Middleware is an object with an `id` and a `handlers` object containing specific event handlers:

```ts
type Middleware<TValue, THandlers extends string> = {
  id: string;
  handlers: {
    [K in THandlers]: MiddlewareHandler<TValue>;
  };
};
```

The `MiddlewareExecutor` registers the middleware:

```ts
// Define middleware with handlers for different events
const textValidationMiddleware = {
  id: "textValidation",
  handlers: {
    // Handler for text changes
    onChange: async ({ state, next, discard }) => {
      if (state.text.length > 100) {
        return discard();
      }
      return next(state);
    },

    // Handler for suggestion selection
    onSuggestionItemSelect: async ({ state, next, complete }) => {
      // Process suggestion...
      return next(state);
    },
  },
};

// Register middleware with executor
middlewareExecutor.use(textValidationMiddleware);
```

The executor runs a handler chain by event name. For example, `onChange` runs all handlers registered for that event. The state type depends on the executor, and each handler receives control functions (`next`, `discard`, etc.).

```ts
// Executor runs the onChange handler chain
middlewareExecutor.execute({
  eventName: "onChange",
  initialValue: { text: "Hello", selection: { start: 1, end: 1 } },
});
```

Each **handler** in the chain receives the current state and **control functions** to manage the execution flow:

- `next(state)`: Continue the execution with the next handler in the chain and with the given state
- `complete(state)`: Stop the execution with and commit the given state
- `discard()`: Stop execution and discard changes
- `forward()`: Skip processing and continue with the next handler

### Middleware Management

Middleware can be registered in several ways:

```ts
// Add middleware
middlewareExecutor.use({
  id: "customMiddleware",
  handlers: {
    onChange: async ({ state, next, complete }) => {
      // Process state
      if (state.text === "done") {
        return complete(state);
      }
      return next(state);
    },
  },
});

// Insert middleware at specific position
middlewareExecutor.insert({
  middleware: [customMiddleware],
  position: { after: "existingMiddlewareId" },
});

// Replace existing middleware
middlewareExecutor.replace([newMiddleware]);

// Set middleware order
middlewareExecutor.setOrder([
  "middleware-1-id",
  "middleware-2-id",
  "middleware-3-id",
]);
```

Middleware can be removed as well:

```ts
// array of middleware IDs
middlewareExecutor.remove([
  "middleware-1-id",
  "middleware-2-id",
  "middleware-3-id",
]);

// or a single middleware ID
middlewareExecutor.remove("middleware-1-id");
```

### Custom Middleware Execution

To create a custom execution chain with custom state, extend `MiddlewareExecutor`. You can customize execution, ordering, and removal behavior.


## Message Composer Middleware Overview

Each manager defines its own handler events and state types. There are two executor types:

1. Executors for state change - handlers mutate the original middleware state value into a new value
2. Composition executors - handlers generate a new value

**TextComposer**

The state-change executor recognizes:

- `onChange`: Handles text changes
- `onSuggestionItemSelect`: Handles suggestion selection

You generally don’t need to access `messageComposer.textComposer.middlewareExecutor` directly; these handlers run inside `TextComposer` methods.

```ts
// internally relies on onChange handler
messageComposer.textComposer.handleChange({
  text,
  selection,
});

// internally relies on onSuggestionItemSelect handler
messageComposer.textComposer.handleSelect(suggestionItem);
```

**AttachmentManager**

Executor for upload preparation recognizes the following handlers:

- `prepare`: actions taken on attachment before a file is uploaded

Executor for post-upload actions recognizes the following handlers:

- `postProcess`: actions taken after the file upload success or failure

**PollComposer**

The state-change executor recognizes:

- `handleFieldChange`: Processes poll form field updates
- `handleFieldBlur`: Handles poll form field blur events

The composition executor recognizes:

- `compose`: Composes poll data before the poll is created server-side

**MessageComposer**

The message composition executor recognizes:

- `compose`: Composes final message data

The draft composition executor recognizes:

- `compose`: Composes final message draft data


## Text Composition Middleware

`TextComposer` state changes (text change or suggestion selection) are processed via middleware. The executor is `TextComposer.middlewareExecutor`.

The executor registers the default middleware with the following middleware factories:

| Factory                                     | Returned middleware ID                                |
| ------------------------------------------- | ----------------------------------------------------- |
| `createTextComposerPreValidationMiddleware` | `'stream-io/text-composer/pre-validation-middleware'` |
| `createMentionsMiddleware`                  | `'stream-io/text-composer/mentions-middleware'`       |
| `createCommandsMiddleware`                  | `'stream-io/text-composer/commands-middleware'`       |

Mentions and commands middleware determine:

- when to trigger suggestions by detecting trigger characters
- how to retrieve suggestions (client-side or server-side pagination) via `MentionsSearchSource` and `CommandSearchSource`

The pre-validation middleware enforces `TextComposer.maxLengthOnEdit`.

### TextComposer Middleware State

`TextComposer` middleware handlers receive a state object that combines `TextComposer` state with a `change` field.

```ts
export type TextComposerMiddlewareExecutorState<
  T extends Suggestion = Suggestion,
> = TextComposerState<T> & {
  change?: {
    selectedSuggestion?: T;
  };
};
```

`change` carries temporary data to merge back into `TextComposer` state after the chain runs. Currently it stores the selected suggestion item.

### TextComposer Middleware Customization

You can customize text composition at different levels. Examples below go from general to specific.

**Change Trigger or Minimum Trigger Characters**

```ts
textComposer.middlewareExecutor.replace([
  createMentionsMiddleware(textComposer.channel, {
    minChars: 3,
    trigger: "__",
  }),
  createCommandsMiddleware(textComposer.channel, { minChars: 3, trigger: "§" }),
]);
```

**Pagination Parameters**

Mentions and commands middleware handle pagination and loading state. Both implement `BaseSearchSource` and support `debounceMs` and `pageSize`.

```ts
const commandSearchSource = new CommandSearchSource(channel, {
  debounceMs,
  pageSize,
});
const mentionSearchSource = new MentionsSearchSource(channel, {
  debounceMs,
  pageSize,
});
```

### Mentions Middleware

| ID                                              | Handlers                             |
| ----------------------------------------------- | ------------------------------------ |
| `'stream-io/text-composer/mentions-middleware'` | `onChange`, `onSuggestionItemSelect` |

### Mentions Middleware Handlers

**OnChange**

The handler has the following responsibilities:

- identify the mention trigger character sequence (`@` by default) at the cursor position
- retrieve the suggestion items in case the trigger has been identified
- pass the suggestion items into the **TextComposer's middleware state** as `suggestions` object

After the chain runs, suggestions appear in `TextComposer` state and therefore in the UI.

**onSuggestionItemSelect**

The handler has the following responsibilities:

- add the selected item into the `mentionedUsers` array of `TextComposer` state
- inject the full mention text into `TextComposer.text` (for example, `@M` → `@Martin`) and adjust cursor position
- reset `suggestions` back to `undefined` which should lead to suggestion UI disappearing

### Custom Mentions Retrieval

Mention suggestions are retrieved via `MentionsSearchSource`. `createMentionsMiddleware` provides a default implementation, or you can supply a custom search source.

### Mentions transliteration

Transliteration maps text to a different script while preserving pronunciation. `MentionsSearchSource` does not transliterate by default; provide a `transliterate` function to enable it. See [MentionsSearchSource Customization section](/chat/docs/sdk/react/components/message-input-components/message-composer-middleware/#mentionssearchsource-customization).

### MentionsSearchSource Configuration

`MentionsSearchSource` allows:

- `debounceMs` - states the debounce interval in milliseconds for pagination requests
- `pageSize` - states the mention suggestion page size during pagination
- `mentionAllAppUsers` - forces the `MentionsSearchSource` instance to always trigger users query instead of just channel members query
- `textComposerText` - allows to keep the whole text composer text, not only the search query extracted from the trigger
- `transliterate` - allows to transliterate the mention names

```ts
import { MentionsSearchSource } from 'stream-chat';
import { default: transliterate } from '@stream-io/transliterate';

const mentionSearchSource = new MentionsSearchSource(channel, {
  debounceMs,
  mentionAllAppUsers,
  pageSize,
  textComposerText,
  transliterate,
});
```

You can also customize retrieval, filtering, transformation, and maintenance.

### Custom MentionsSearchSource

Custom `searchSource` implementations should follow the `MentionsSearchSource` interface. Provide one like this:

```ts
const searchSource = new CustomMentionsSearchSource();

textComposer.middlewareExecutor.replace([
  createMentionsMiddleware(channel, { searchSource }),
]);
```

The custom instance can customize the following behavior:

**Query Parameters Customization**

Query parameters are `filters`, `sort` and `options`. These can be customized by overriding `MentionsSearchSource.prepareQueryUsersParams` and `MentionsSearchSource.prepareQueryMembersParams` methods.

```ts
import { MentionsSearchSource } from "stream-chat";
import type {
  UserFilters,
  UserSort,
  UserOptions,
  MemberFilters,
  MemberSort,
  UserOptions,
} from "stream-chat";

class CustomMentionsSearchSource extends MentionsSearchSource {
  // ...
  prepareQueryUsersParams = (
    searchQuery: string,
  ): { filters: UserFilters; sort: UserSort; options: UserOptions } => {
    //... generate filters, sort, options
    return { filers, sort, options };
  };
  prepareQueryMembersParams = (
    searchQuery: string,
  ): { filters: MemberFilters; sort: MemberSort; options: UserOptions } => {
    //... generate filters, sort, options
    return { filers, sort, options };
  };
}
```

If parameters don’t depend on the search query, you can set them directly (usually not for filters):

```ts
mentionsSearchSource.userSort = { name: -1 };
mentionsSearchSource.memberSort = { name: -1 };
mentionsSearchSource.searchOptions = { include_deactivated_users: true };
```

**Custom Pagination Logic**

We can customize how the searched items are retrieved by overriding the `MentionsSearchSource` method `query`. If the pagination supports cursor, the `next` value (cursor) should also be returned. We expect it to be a string:

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

class CustomMentionsSearchSource extends MentionsSearchSource {
  // ...
  query = async (searchQuery: string) => {
    let result;
    // custom logic...

    return {
      items: result.items,
      next: result.cursor,
    };
  };
}
```

**Custom Results Filtering**

Apply custom filtering logic to the query results before they are committed to the search source state.

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

const mentionsFilter = (item: UserSuggestion) => {
  //... custom filtering logic
};

class CustomMentionsSearchSource extends MentionsSearchSource {
  // ...
  filterQueryResults = (items: UserSuggestion[]) => {
    return items.filter(mentionsFilter);
  };
}
```

**State Retention Between Searches**

On a new search query, state resets to the initial form except for `items`, which are kept until new results arrive. This reduces UI flicker between empty state and first-page results. You can override `getStateBeforeFirstQuery` to change this:

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

class CustomMentionsSearchSource extends MentionsSearchSource {
  // ...
  getStateBeforeFirstQuery = (newSearchString: string) => {
    return {
      ...super.getStateBeforeFirstQuery(newSearchString),
      items: [],
    };
  };
}
```

### Commands Middleware

| ID                                              | Handlers                             |
| ----------------------------------------------- | ------------------------------------ |
| `'stream-io/text-composer/commands-middleware'` | `onChange`, `onSuggestionItemSelect` |

### Commands Middleware Handlers

**OnChange**

The handler has the following responsibilities:

- identify the command trigger character sequence (`/` by default) at the cursor position
- retrieve the suggestion items in case the trigger has been identified
- pass the suggestion items into the **TextComposer's middleware state** as `suggestions` object

After the chain runs, suggestions appear in `TextComposer` state and therefore in the UI.

**onSuggestionItemSelect**

The handler has the following responsibilities:

- set the selected item to `command` field of `TextComposer` state
- inject the full command text into `TextComposer.text` (for example, `/g` → `/giphy`) and adjust cursor position
- reset `suggestions` back to `undefined` which should lead to suggestion UI disappearing

### Custom Command Retrieval

By default, command data is retrieved via `CommandSearchSource`. You can provide a custom source by extending `CommandSearchSource`.

### CommandSearchSource Configuration

`CommandSearchSource` allows:

- `debounceMs` - states the debounce interval in milliseconds for pagination requests
- `pageSize` - states the mention suggestion page size during pagination

You can also customize retrieval, filtering, transformation, and maintenance.

### Custom CommandSearchSource

If you need to redefine suggestion retrieval, provide a custom search source:

```ts
const searchSource = new CustomCommandSearchSource();

textComposer.middlewareExecutor.replace([
  createCommandsMiddleware(textComposer.channel, { searchSource }),
]);
```

The custom instance can customize the following behavior:

**Custom Pagination Logic**

We can customize how the searched items are retrieved by overriding the `CommandSearchSource` method `query`. If the pagination supports cursor, the `next` value (cursor) should also be returned. We expect it to be a string:

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

class CustomCommandSearchSource extends CommandSearchSource {
  // ...
  query = async (searchQuery: string) => {
    let result;
    // custom logic...

    return {
      items: result.items,
      next: result.cursor,
    };
  };
}
```

**Custom Results Filtering**

Apply custom filtering logic to the query results before they are committed to the search source state.

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

const commandFilter = (item: CommandSuggestion) => {
  //... custom filtering logic
};

class CustomMentionsSearchSource extends CommandSearchSource {
  // ...
  filterQueryResults = (items: CommandSuggestion[]) => {
    return items.filter(commandFilter);
  };
}
```

**State Retention Between Searches**

On a new search query, state resets to the initial form except for `items`, which are kept until new results arrive. This reduces UI flicker between empty state and first-page results. You can override `getStateBeforeFirstQuery` to change this:

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

class CustomCommandSearchSource extends CommandSearchSource {
  // ...
  getStateBeforeFirstQuery = (newSearchString: string) => {
    return {
      ...super.getStateBeforeFirstQuery(newSearchString),
      items: [],
    };
  };
}
```

[//]: # "### Command UI Middleware"
[//]: #
[//]: # 'The `command` state is set in `TextComposer` via `createCommandsMiddleware`, which handles command suggestions and execution.

Use `command` to render a custom command UI in the message input. It contains the command response.

`stream-chat` provides the following middlewares for command processing in the Message Composer UI:

- `createActiveCommandGuardMiddleware`(`stream-io/text-composer/active-command-guard`) - This middleware ensures that if the command state is set, the input will complete the execution with the current state.
- `createCommandStringExtractionMiddleware`(`stream-io/text-composer/command-string-extraction`) - Extracts the command string from input text. Example: `/ban user` becomes `user`.
- `createCommandInjectionMiddleware`(`stream-io/message-composer-middleware/command-string-injection`) - Injects the command string into message composer state. Example: `user` is sent as `/ban user`.
- `createDraftCommandInjectionMiddleware`(`stream-io/message-composer-middleware/draft-command-string-injection`) - Injects the command string into the draft state. Example: `user` is saved as `/ban user`.
'

### Add Custom Suggestion Type

To add a new suggestion type and associate it with a trigger:

1. Create a new middleware.
2. Insert it at the appropriate position in the execution flow.

```ts
import { ChannelSearchSource } from "stream-chat";
import type {
  Channel,
  MessageComposer,
  TextComposerMiddlewareExecutorState,
} from "stream-chat";

type ChannelWithId = Channel & { id: string };

type ChannelMentionsMiddleware = Middleware<
  TextComposerMiddlewareExecutorState<ChannelWithId>,
  "onChange" | "onSuggestionItemSelect"
>;
// Custom middleware for emoji suggestions
const createTextComposerChannelMiddleware: TextComposerMiddleware = (
  composer: MessageComposer,
) => {
  const trigger = "#";
  const channelSearchSource = new ChannelSearchSource(composer.client);
  return {
    id: "text-composer/channels-middleware",
    handlers: {
      onChange: async ({ state, forward, next }) => {
        const { text } = state;
        if (!text) return forward();
        const newState = {};

        // identifying the trigger, removing existin suggestions ...

        return next(newState);
      },
      onSuggestionItemSelect: async ({ state, forward, next }) => {
        const { selectedSuggestion: channel } = state.change;

        return next({
          ...state,
          text: insertTextAtSelection(state.text, channel, state.selection),
          suggestions: undefined,
        });
      },
    },
  };
};

// Register the middleware
messageComposer.textComposer.middlewareExecutor.insert({
  middleware: [createTextComposerChannelMiddleware(messageComposer)],
  position: { before: "stream-io/text-composer/mentions-middleware" },
});
```


## Poll Composition Middleware

`PollComposer` state changes and composition run through two middleware executors:

**`PollComposer.stateMiddlewareExecutor`**

Handles poll form field validation and updates.

Registers the default middleware with the following middleware factories:

| Factory                             | Returned middleware ID                       | Recognized handler names               |
| ----------------------------------- | -------------------------------------------- | -------------------------------------- |
| `createPollComposerStateMiddleware` | `'stream-io/poll-composer-state-processing'` | `handleFieldChange`, `handleFieldBlur` |

**`PollComposer.compositionMiddlewareExecutor`**

Validates the final poll composition before creation.

Registers the default middleware with the following middleware factories:

| Factory                                     | Returned middleware ID                  | Recognized handler names |
| ------------------------------------------- | --------------------------------------- | ------------------------ |
| `createPollCompositionValidationMiddleware` | `'stream-io/poll-composer-composition'` | `compose`                |

### PollComposer State Middleware Customization

This middleware chain transforms and validates the poll data. Customization focuses on:

- **processors** - functions that transform field values
- **validators** - functions that validate field values

These run when a field changes or blurs (`handleFieldChange` and `handleFieldBlur`).

The custom processor and validator functions are supplied to the middleware factory function:

```ts
const stateMiddleware = createPollComposerStateMiddleware({
  processors: customProcessors,
  validators: customValidators,
}: PollComposerStateMiddlewareFactoryOptions);
```

These are defined as:

```ts
export type PollComposerStateMiddlewareFactoryOptions = {
  processors?: {
    handleFieldChange?: Partial<
      Record<keyof PollComposerState["data"], PollCompositionStateProcessor>
    >;
    handleFieldBlur?: Partial<
      Record<keyof PollComposerState["data"], PollCompositionStateProcessor>
    >;
  };
  validators?: {
    handleFieldChange?: Partial<
      Record<keyof PollComposerState["data"], PollStateChangeValidator>
    >;
    handleFieldBlur?: Partial<
      Record<keyof PollComposerState["data"], PollStateChangeValidator>
    >;
  };
};
```

An example of custom validators:

```ts
const customValidators = {
  handleFieldChange: {
    name: ({ value }) => {
      if (value.length < 3)
        return { name: "Name must be at least 3 characters" };
      return { name: undefined };
    },
  },
};
```

A validator checks a single field and returns an object whose key is the field name and whose value is:

- `string` error message if invalid, or
- `undefined` if valid

In the example above, `name` is validated in `handleFieldChange`.

An example of custom processors:

```ts
const customProcessors = {
  handleFieldChange: {
    options: ({ value, data }) => {
      // Custom option processing logic
      return {
        options: value.map((option) => ({
          ...option,
          text: option.text.toUpperCase(),
        })),
      };
    },
  },
};
```

Processor functions return an object keyed by field name with the value to commit. In the example above, option text is uppercased.

### The State Change Middleware Value

Every `stateMiddlewareExecutor` handler receives a `state` value with:

- `previousState`: The state (`PollComposerState`) before the current change
- `nextState`: The next state (`PollComposerState`) to be committed after successful middleware execution
- `targetFields`: The fields being updated in the current change

The `PollComposerState` type includes:

```ts
type PollComposerState = {
  data: {
    allow_answers: boolean;
    allow_user_suggested_options: boolean;
    description: string;
    enforce_unique_vote: boolean;
    id: string;
    max_votes_allowed: string;
    name: string;
    options: Array<{ id: string; text: string }>;
    user_id?: string;
    voting_visibility: VotingVisibility;
  };
  errors: Record<string, string>;
};
```

### Poll Composition Middleware

The composition middleware validates the final poll state before sending it to the server (for example, no errors and at least one option). The default factory has no customization parameters, but you can replace it using the standard middleware pattern:

```ts
// appends the custom middleware
messageComposer.pollComposer.compositionMiddlewareExecutor.use({
  id: "custom-id-you-choose",
  handlers: {
    compose: ({
      discard,
      forward,
    }: MiddlewareHandlerParams<PollComposerCompositionMiddlewareValueState>) => {
      if (customValidation()) return forward();
      return discard();
    },
  },
});
```


## Message Composition Middleware

You can inject custom message data via middleware handlers or by setting data directly.

`MessageComposer` uses middleware executors for message and draft composition:

- `MessageComposer.compositionMiddlewareExecutor` - for message composition
- `MessageComposer.draftCompositionMiddlewareExecutor` - for draft composition

### Default Middleware

#### MessageComposerMiddlewareState Structure

`MessageComposerMiddlewareState` is the core middleware state with three fields:

```typescript
export type MessageComposerMiddlewareState = {
  message: Message | UpdatedMessage;
  localMessage: LocalMessage;
  sendOptions: SendMessageOptions;
};
```

#### Field Definitions

1. **localMessage**: `LocalMessage`
   - Used for local channel state updates
   - Shown in the UI immediately
   - Can include temporary data or UI-only properties (IDs, timestamps, flags)

2. **message**: `Message | UpdatedMessage`
   - Data sent to the backend for create/update
   - Should match the backend message shape

3. **sendOptions**: `SendMessageOptions | UpdateMessageOptions`
   - Options for sending/updating the message

#### Composition Middleware Executor

The composition middleware executor registers the following middleware in order:

| Order | Factory                                           | Returned middleware ID                                     | Handler   | Role                                                                                                                           |
| ----- | ------------------------------------------------- | ---------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ |
| 1.    | `createTextComposerCompositionMiddleware`         | `'stream-io/message-composer-middleware/text-composition'` | `compose` | Adds text and updates mentioned users based on the current text.                                                               |
| 2.    | `createAttachmentsCompositionMiddleware`          | `'stream-io/message-composer-middleware/attachments'`      | `compose` | Adds `AttachmentManager.attachments` to both `localMessage` and `message`. Discards composition if any uploads are unfinished. |
| 3.    | `createLinkPreviewsCompositionMiddleware`         | `'stream-io/message-composer-middleware/link-previews'`    | `compose` | Adds loaded link previews to attachments and decides whether to skip server-side enrichment.                                   |
| 4.    | `createSharedLocationCompositionMiddleware`       | `'stream-io/message-composer-middleware/shared-location'`  | `compose` | Adds `shared_location` to `localMessage` and `message`.                                                                        |
| 5.    | `createMessageComposerStateCompositionMiddleware` | `'stream-io/message-composer-middleware/own-state'`        | `compose` | Adds `MessageComposer` state values (`poll_id`, `quoted_message_id`, `show_in_channel`).                                       |
| 6.    | `createCustomDataCompositionMiddleware`           | `'stream-io/message-composer-middleware/custom-data'`      | `compose` | Adds `CustomDataManager.customMessageData`.                                                                                    |
| 7.    | `createCompositionValidationMiddleware`           | `'stream-io/message-composer-middleware/data-validation'`  | `compose` | Enforces `maxLengthOnSend`. Discards if empty or unchanged (based on `message.updated` and `draft.updated` WS events).         |
| 8.    | `createCompositionDataCleanupMiddleware`          | `'stream-io/message-composer-middleware/data-cleanup'`     | `compose` | Converts `message` to `UpdatedMessage` and `sendOptions` to `UpdateMessageOptions`.                                            |

#### Draft Composition Middleware Executor

The draft composition middleware executor (`messageComposer.draftCompositionMiddlewareExecutor`) registers:

| Order | Factory                                                | Returned middleware ID                                           | Handler   | Role                                                                                                                                                           |
| ----- | ------------------------------------------------------ | ---------------------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1.    | `createDraftTextComposerCompositionMiddleware`         | `'stream-io/message-composer-middleware/draft-text-composition'` | `compose` | Adds text and updates mentioned users based on the current text.                                                                                               |
| 2.    | `createDraftAttachmentsCompositionMiddleware`          | `'stream-io/message-composer-middleware/draft-attachments'`      | `compose` | Adds `AttachmentManager.attachments` to both `localMessage` and `message`. Keeps drafts even if uploads are in progress, but only includes successful uploads. |
| 3.    | `createDraftLinkPreviewsCompositionMiddleware`         | `'stream-io/message-composer-middleware/draft-link-previews'`    | `compose` | Adds loaded link previews to attachments.                                                                                                                      |
| 4.    | `createDraftMessageComposerStateCompositionMiddleware` | `'stream-io/message-composer-middleware/draft-own-state'`        | `compose` | Adds `MessageComposer` state values (`poll_id`, `quoted_message_id`, `show_in_channel`).                                                                       |
| 5.    | `createDraftCustomDataCompositionMiddleware`           | `'stream-io/message-composer-middleware/draft-custom-data'`      | `compose` | Adds `CustomDataManager.customMessageData`.                                                                                                                    |
| 6.    | `createDraftCompositionDataCleanupMiddleware`          | `'stream-io/message-composer-middleware/draft-data-validation'`  | `compose` | Verifies the draft can be created and discards if empty.                                                                                                       |

### Message Composition Customization

**Custom Message Data Transformation**

Add custom transformation logic by adding a composition middleware with a `compose` handler:

```ts
import type {
  MessageCompositionMiddleware,
  MessageComposerMiddlewareValueState,
  MiddlewareHandlerParams,
} from "stream-chat";

const createCustomCompositionMiddleware = (
  composer: MessageComposer,
): MessageCompositionMiddleware => ({
  id: "message-composer/message-composer/custom-message-data",
  handlers: {
    compose: async ({
      state,
      next,
      forward,
    }: MiddlewareHandlerParams<MessageComposerMiddlewareValueState>) => {
      if (!composer.textComposer) return forward();

      const { mentionedUsers, text } = composer.textComposer;

      // Create new state objects
      const newLocalMessage = {
        ...state.localMessage,
        custom_ui_flag: true,
      };

      const newMessage = {
        ...state.message,
        custom_field: "value",
        mentioned_users: mentionedUsers.map((u) => u.id),
        text: text.toUpperCase(),
      };

      // Pass updated state to next middleware
      return next({
        ...state,
        localMessage: newLocalMessage,
        message: newMessage,
      });
    },
  },
});
```

Insert the middleware into the correct executor chain at the correct position:

```ts
messageComposer.compositionMiddlewareExecutor.insert({
  middleware: [createCustomCompositionMiddleware(messageComposer)],
  position: { after: "stream-io/message-composer-middleware/data-cleanup" },
});
```

**Draft Composition Customization**

Similarly, you can customize draft composition:

```ts
import type {
  MessageDraftCompositionMiddleware,
  MessageDraftComposerMiddlewareValueState,
  MiddlewareHandlerParams,
} from "stream-chat";

const createCustomDraftMiddleware = (
  composer: MessageComposer,
): MessageDraftCompositionMiddleware => ({
  id: "message-composer/message-composer/custom-draft",
  handlers: {
    compose: async ({
      state,
      next,
      forward,
    }: MiddlewareHandlerParams<MessageDraftComposerMiddlewareValueState>) => {
      if (!composer.textComposer) return forward();

      const { text } = composer.textComposer;

      // Create new draft state
      const newDraft = {
        ...state.draft,
        custom_field: "draft_value",
        text: text.toLowerCase(),
      };

      // Pass updated state to next middleware
      return next({
        ...state,
        draft: newDraft,
      });
    },
  },
});
```

Insert the middleware into the correct executor chain at the correct position:

```ts
messageComposer.draftCompositionMiddlewareExecutor.insert({
  middleware: [createCustomDraftMiddleware(messageComposer)],
  position: {
    after: "stream-io/message-composer-middleware/draft-data-validation",
  },
});
```

### Middleware Management

**Registering Custom Middleware**

When adding custom middleware, you can insert it at specific positions using the middleware IDs:

```ts
// Add middleware after text composition
messageComposer.compositionMiddlewareExecutor.insert({
  middleware: [customCompositionMiddleware],
  position: { after: "stream-io/message-composer-middleware/text-composition" },
});

// Add middleware before draft composition
messageComposer.draftCompositionMiddlewareExecutor.insert({
  middleware: [customDraftMiddleware],
  position: {
    before: "stream-io/message-composer-middleware/draft-text-composition",
  },
});

// Replace existing middleware
messageComposer.compositionMiddlewareExecutor.replace([newMiddleware]);

// Set middleware order
messageComposer.compositionMiddlewareExecutor.setOrder([
  "stream-io/message-composer-middleware/text-composition",
  "custom-composition",
  "stream-io/message-composer-middleware/attachments",
  "stream-io/message-composer-middleware/link-previews",
  "stream-io/message-composer-middleware/custom-data",
  "stream-io/message-composer-middleware/data-validation",
  "stream-io/message-composer-middleware/data-cleanup",
]);
```


### Emoji Suggestions Middleware

Emoji suggestions are handled by `TextComposer` middleware and are disabled by default. Enable them as follows:

```tsx
import { createTextComposerEmojiMiddleware } from "stream-chat-react-native";
import type { TextComposerMiddleware } from "stream-chat";

import { init, SearchIndex } from "emoji-mart";
import data from "@emoji-mart/data";

init({ data });

const Component = () => {
  useEffect(() => {
    if (!chatClient) return;

    chatClient.setMessageComposerSetupFunction(({ composer }) => {
      composer.textComposer.middlewareExecutor.insert({
        middleware: [
          createTextComposerEmojiMiddleware({
            emojiSearchIndex: SearchIndex,
          }) as TextComposerMiddleware,
        ],
        position: { before: "stream-io/text-composer/mentions-middleware" },
        unique: true,
      });
    });
  }, [chatClient]);
};
```

You can customize the middleware:

**Change Trigger or Minimum Trigger Characters**

```ts
textComposer.middlewareExecutor.use([
  createTextComposerEmojiMiddleware(SearchIndex, {
    minChars: 3,
    trigger: "__",
  }),
]);
```

**Provide Custom Search Source**

To customize how suggestions are retrieved, provide a custom search source:

```ts
const searchSource = new CustomSearchSource();

textComposer.middlewareExecutor.use([
  createTextComposerEmojiMiddleware(searchSource),
]);
```

### Command UI Middleware

The `command` state is set in `TextComposer` via `createCommandsMiddleware`, which handles command suggestions and execution.

Use `command` to render a custom command UI in the message input. It contains the command response.

`stream-chat` provides the following middlewares for command processing in the Message Composer UI:

- `createActiveCommandGuardMiddleware`(`stream-io/text-composer/active-command-guard`) - This middleware ensures that if the command state is set, the input will complete the execution with the current state.
- `createCommandStringExtractionMiddleware`(`stream-io/text-composer/command-string-extraction`) - Extracts the command string from input text. Example: `/ban user` becomes `user`.
- `createCommandInjectionMiddleware`(`stream-io/message-composer-middleware/command-string-injection`) - Injects the command string into message composer state. Example: `user` is sent as `/ban user`.
- `createDraftCommandInjectionMiddleware`(`stream-io/message-composer-middleware/draft-command-string-injection`) - Injects the command string into the draft state. Example: `user` is saved as `/ban user`.



---

This page was last updated at 2026-04-17T17:33:44.925Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v8/ui-components/message-input/composer/message-composer-middleware/](https://getstream.io/chat/docs/sdk/react-native/v8/ui-components/message-input/composer/message-composer-middleware/).