type Middleware<TValue, THandlers extends string> = {
id: string;
handlers: {
[K in THandlers]: MiddlewareHandler<TValue>;
};
};MessageComposer Middleware
Message Composer Middleware
MessageComposer middleware gives full control over data processing in different composition scenarios.
Best Practices
- Keep middleware order intentional to avoid unexpected transformations.
- Use unique IDs for middleware to prevent duplicate registrations.
- Scope middleware by context (channel/thread/edit) when needed.
- Keep middleware pure where possible to simplify debugging.
- Test edge cases (mentions, emoji, attachments) after middleware changes.
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:
The MiddlewareExecutor registers the middleware:
// 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.).
// 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 statecomplete(state): Stop the execution with and commit the given statediscard(): Stop execution and discard changesforward(): Skip processing and continue with the next handler
Middleware Management
Middleware can be registered in several ways:
// 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:
// 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:
- Executors for state change - handlers mutate the original middleware state value into a new value
- Composition executors - handlers generate a new value
TextComposer
The state-change executor recognizes:
onChange: Handles text changesonSuggestionItemSelect: Handles suggestion selection
You generally don’t need to access messageComposer.textComposer.middlewareExecutor directly; these handlers run inside TextComposer methods.
// 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 updateshandleFieldBlur: 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
MentionsSearchSourceandCommandSearchSource
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.
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
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.
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
suggestionsobject
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
mentionedUsersarray ofTextComposerstate - inject the full mention text into
TextComposer.text(for example,@M→@Martin) and adjust cursor position - reset
suggestionsback toundefinedwhich 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.
MentionsSearchSource Configuration
MentionsSearchSource allows:
debounceMs- states the debounce interval in milliseconds for pagination requestspageSize- states the mention suggestion page size during paginationmentionAllAppUsers- forces theMentionsSearchSourceinstance to always trigger users query instead of just channel members querytextComposerText- allows to keep the whole text composer text, not only the search query extracted from the triggertransliterate- allows to transliterate the mention names
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:
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.
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):
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:
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.
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:
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
suggestionsobject
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
commandfield ofTextComposerstate - inject the full command text into
TextComposer.text(for example,/g→/giphy) and adjust cursor position - reset
suggestionsback toundefinedwhich 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 requestspageSize- 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:
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:
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.
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:
import { CommandSearchSource } from "stream-chat";
class CustomCommandSearchSource extends CommandSearchSource {
// ...
getStateBeforeFirstQuery = (newSearchString: string) => {
return {
...super.getStateBeforeFirstQuery(newSearchString),
items: [],
};
};
}Add Custom Suggestion Type
To add a new suggestion type and associate it with a trigger:
- Create a new middleware.
- Insert it at the appropriate position in the execution flow.
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" },
});Emoji Suggestions Middleware
Emoji suggestions are handled by TextComposer middleware. It’s disabled by default. To enable it, import it from stream-chat-react/emojis and register it:
import { createTextComposerEmojiMiddleware } from "stream-chat-react/emojis";
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(
SearchIndex,
) as TextComposerMiddleware,
],
position: { before: "stream-io/text-composer/mentions-middleware" },
unique: true,
});
});
}, [chatClient]);
};You can customize the middleware in a few ways:
Change Trigger or Minimum Trigger Characters
textComposer.middlewareExecutor.use([
createTextComposerEmojiMiddleware(SearchIndex, {
minChars: 3,
trigger: "__",
}),
]);Provide Custom Search Source
If you need custom suggestions, provide a custom search source:
const searchSource = new CustomSearchSource();
textComposer.middlewareExecutor.use([
createTextComposerEmojiMiddleware(searchSource),
]);Attachment Upload Middleware
Since stream-chat@9.14.0, you can customize actions before and after attachment upload by registering middleware handlers for these events:
prepare- handlers with this name are executed before the file uploadpostProcess- handlers with this name run after the file upload
Pre-upload executor registers default middleware via these factories:
| Factory | Returned middleware ID | Recognized handler names |
|---|---|---|
createUploadConfigCheckMiddleware | 'stream-io/attachment-manager-middleware/file-upload-config-check' | prepare |
createBlockedAttachmentUploadNotificationMiddleware | 'stream-io/attachment-manager-middleware/blocked-upload-notification' | prepare |
Post-upload executor registers default middleware via these factories:
| Factory | Returned middleware ID | Recognized handler names |
|---|---|---|
createUploadErrorHandlerMiddleware | 'stream-io/attachment-manager-middleware/upload-error' | postProcess |
createPostUploadAttachmentEnrichmentMiddleware | 'stream-io/attachment-manager-middleware/post-upload-enrichment' | postProcess |
Middleware registration uses the standard pattern. Example:
chatClient.setMessageComposerSetupFunction(({ composer }) => {
// must be registered with preUploadMiddlewareExecutor
composer.attachmentManager.preUploadMiddlewareExecutor.insert({
middleware: [createCustomPreUploadMiddleware(composer)],
position: {
after:
"stream-io/attachment-manager-middleware/blocked-upload-notification",
},
unique: true,
});
// must be registered with postUploadMiddlewareExecutor
composer.attachmentManager.postUploadMiddlewareExecutor.insert({
middleware: [createCustomPostUploadMiddleware(composer)],
position: {
before: "stream-io/attachment-manager-middleware/post-upload-enrichment",
},
unique: true,
});
});Default attachment upload middleware handlers
To position custom middleware correctly, note the default order for each event:
prepare
stream-io/attachment-manager-middleware/file-upload-config-check- checks Stream CDN upload config and attachesuploadPermissionChecktoattachment.localMetadata.stream-io/attachment-manager-middleware/blocked-upload-notification- error notification is emitted via theclient.notificationsservice if theuploadPermissionCheck.uploadBlockedistrue.
postProcess
stream-io/attachment-manager-middleware/upload-error- error notification is emitted via theclient.notificationsservice if the upload request failed and the middleware pipeline initial state containserror.stream-io/attachment-manager-middleware/post-upload-enrichment- the attachment object is enriched with propertiesimage_url(image attachment),asset_url(non-image attachment),thumb_url(the upload response containedthumb_urlproperty). In case of image attachments thepreviewUriis removed fromattachment.localMetadata.
Attachment upload middleware executor states
There are two middleware executors: one for upload preparation and one for post-processing. Each uses a different state shape:
Handlers executed by preUploadMiddlewareExecutor have access only to attachment:
import type {
AttachmentPreUploadMiddleware,
AttachmentPreUploadMiddlewareState,
LocalUploadAttachment,
MessageComposer,
MiddlewareHandlerParams,
} from "stream-chat";
import { customPermissionCheck } from "./uploadPermissionCheck";
export const createCustomPreUploadMiddleware = (
composer: MessageComposer,
): AttachmentPreUploadMiddleware => ({
id: "custom-file-upload-config-check",
handlers: {
prepare: async ({
state, // only {attachment: LocalUploadAttachment}
next,
discard,
}: MiddlewareHandlerParams<AttachmentPreUploadMiddlewareState>) => {
const { attachmentManager } = composer;
if (!attachmentManager || !state.attachment) return discard();
const uploadPermissionCheck = await customPermissionCheck(attachment);
const attachment: LocalUploadAttachment = {
...state.attachment,
localMetadata: {
...state.attachment.localMetadata,
uploadPermissionCheck,
uploadState: uploadPermissionCheck.uploadBlocked
? "blocked"
: "pending",
},
};
return next({
...state,
attachment,
});
},
},
});Handlers executed by postUploadMiddlewareExecutor have access to error, response, and attachment:
import type {
AttachmentPostUploadMiddleware,
AttachmentPostUploadMiddlewareState,
MessageComposer,
MiddlewareHandlerParams,
} from "stream-chat";
export const createCustomErrorHandlerMiddleware = (
composer: MessageComposer,
): AttachmentPostUploadMiddleware => ({
id: "custom-upload-error-middleware",
handlers: {
postProcess: ({
state, // { attachment, error, response }
discard,
forward,
}: MiddlewareHandlerParams<AttachmentPostUploadMiddlewareState>) => {
const { attachment, error } = state;
if (!error) return forward();
if (!attachment) return discard();
const reason = error instanceof Error ? error.message : "unknown error";
composer.client.notifications.addError({
message: "Error uploading attachment",
origin: {
emitter: "AttachmentManager",
context: { attachment },
},
options: {
type: "api:attachment:upload:failed",
metadata: { reason },
originalError: error,
},
});
return forward();
},
},
});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:
const stateMiddleware = createPollComposerStateMiddleware({
processors: customProcessors,
validators: customValidators,
}: PollComposerStateMiddlewareFactoryOptions);These are defined as:
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:
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:
stringerror message if invalid, orundefinedif valid
In the example above, name is validated in handleFieldChange.
An example of custom processors:
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 changenextState: The next state (PollComposerState) to be committed after successful middleware executiontargetFields: The fields being updated in the current change
The PollComposerState type includes:
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:
// 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 compositionMessageComposer.draftCompositionMiddlewareExecutor- for draft composition
Default Middleware
MessageComposerMiddlewareState Structure
MessageComposerMiddlewareState is the core middleware state with three fields:
export type MessageComposerMiddlewareState = {
message: Message | UpdatedMessage;
localMessage: LocalMessage;
sendOptions: SendMessageOptions;
};Field Definitions
localMessage:
LocalMessage- Used for local channel state updates
- Shown in the UI immediately
- Can include temporary data or UI-only properties (IDs, timestamps, flags)
message:
Message | UpdatedMessage- Data sent to the backend for create/update
- Should match the backend message shape
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:
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:
messageComposer.compositionMiddlewareExecutor.insert({
middleware: [createCustomCompositionMiddleware(messageComposer)],
position: { after: "stream-io/message-composer-middleware/data-cleanup" },
});Draft Composition Customization
Similarly, you can customize draft composition:
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:
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:
// 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",
]);- Message Composer Middleware
- Best Practices
- Message Composer Middleware Overview
- Text Composition Middleware
- TextComposer Middleware State
- TextComposer Middleware Customization
- Mentions Middleware
- Mentions Middleware Handlers
- Custom Mentions Retrieval
- Mentions transliteration
- MentionsSearchSource Configuration
- Custom MentionsSearchSource
- Commands Middleware
- Commands Middleware Handlers
- Custom Command Retrieval
- CommandSearchSource Configuration
- Custom CommandSearchSource
- Add Custom Suggestion Type
- Emoji Suggestions Middleware
- Attachment Upload Middleware
- Poll Composition Middleware
- Message Composition Middleware