# MessageComposer Class

`MessageComposer` is the central orchestrator for message creation and editing. It:

- Manages the state of message composition
- Handles different types of content (text, attachments, polls, etc.)
- Provides draft functionality for saving message progress
- Supports message editing and quoting
- Integrates with the Stream Chat API for message operations

Instantiate the composer like this:

```ts
new MessageComposer({
  client, // StreamChat client instance
  compositionContext, // it is necessary to provide a Channel or Thread instance or LocalMessage object
  composition, // optional initial state like draft message or edited message
  config, // optional custom configuration
});
```

Each `Channel` and `Thread` instance creates its own `messageComposer`. To edit a message, pass the `LocalMessage` object to both `compositionContext` and `composition`.

`MessageComposer` uses modular managers for specific concerns:

- **TextComposer**: text input, commands, mentions
- **AttachmentManager**: attachments state and uploads
- **LinkPreviewsManager**: link preview processing
- **LocationComposer**: `message.shared_location` composition
- **PollComposer**: poll composition
- **CustomDataManager**: custom message and non-message data

Each manager operates independently and keeps its own dedicated state. `MessageComposer` orchestrates their interactions.

`MessageComposer` features:

- **State Management**: reactive state tracking
- **Middleware Support**: custom processing via middleware
- **Draft System**: save/restore drafts
- **Rich Content**: text, attachments, polls, link previews
- **Event System**: real-time subscriptions
- **Configurable**: via `MessageComposerConfig`


<admonition type="info">

**New in v8**: The `MessageComposer` is a significant architectural change from v7. If you're upgrading, see the [migration guide](/chat/docs/sdk/react-native/v8/basics/upgrading-from-v7/) for details on how to update your code.

</admonition>

## Best Practices

- Register a single setup function per client to avoid duplicated middleware.
- Scope middleware by `contextType` so thread/edit flows behave correctly.
- Keep middleware ordering stable and use `unique: true` to avoid duplicates.
- Use `useMessageComposer` to access the correct composer for the current context.
- Clean up or re-run setup only when relevant dependencies change.

### StreamChat.setMessageComposerSetupFunction

Set one `MessageComposer` setup function per client. Ideally, set it once (or when dependencies change).

```tsx
import { defaultTextComposerMiddlewares } from "stream-chat";

const chatClient = useCreateChatClient({
  apiKey,
  tokenOrProvider: userToken,
  userData: { id: userId, language: "en" },
});

const [emojisEnabled, setEmojisEnabled] = useState(false);

useEffect(() => {
  chatClient.setMessageComposerSetupFunction(({ composer }) => {
    if (composer.contextType === "channel") {
      composer.textComposer.insert(
        defaultTextComposerMiddlewares.map(composer.channel),
      );

      if (emojisEnabled) {
        composer.textComposer.middlewareExecutor.insert({
          middleware: [
            createTextComposerEmojiMiddleware(
              SearchIndex,
            ) as TextComposerMiddleware,
          ],
          position: { after: "stream-io/text-composer/mentions-middleware" },
          unique: true,
        });
      }
    }
  });
}, [chatClient, emojisEnabled]);
```

### Message Composer Configuration

`MessageComposer` supports configuration at three levels: global (client-wide), per-instance, and per-manager.

### Configuration Structure

The configuration is defined by the `MessageComposerConfig` interface:

```ts
type MessageComposerConfig = {
  drafts: {
    enabled: boolean; // Enables server-side draft functionality. Disabled by default.
  };
  attachments: {
    /**
     * Function to filter out files before being uploaded. The default forwards without filtering.
     */
    fileUploadFilter: (file: FileLike) => boolean;
    /**
     * Maximum file upload attachments per message. Default is 10.
     */
    maxNumberOfFilesPerMessage: number;
    /**
     * Allowed file types.
     * Expected are unique file type specifiers
     * (https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept#unique_file_type_specifiers).
     * The default is an empty array - all files accepted.
     */
    acceptedFiles: string[];
    /**
     * When true (default), the attachment manager updates `localMetadata.uploadProgress` during
     * uploads and supplies `options.onProgress` to `doUploadRequest` (custom uploads) and to the
     * default channel upload (axios `onUploadProgress`). Set to `false` to leave `uploadProgress`
     * undefined and skip progress callbacks.
     */
    trackUploadProgress: boolean;
    /**
     * Custom upload request. The optional second argument includes `onProgress`; call it with a
     * 0–100 value (or `undefined` when indeterminate) so preview UI can stay in sync when
     * `trackUploadProgress` is enabled.
     */
    doUploadRequest?: (
      file: FileLike,
      options?: { onProgress?: (percent: number | undefined) => void },
    ) => Promise<
      { file: string; thumb_url?: string } & Partial<Record<string, unknown>>
    >;
  };
  linkPreviews: {
    /**
     * Enables link previews. Disabled by default.
     */
    enabled: boolean;
    /**
     * Debounce time for URL detection and enrichment. By default 1500ms.
     */
    debounceURLEnrichmentMs: number;
    /**
     * Custom URL detection function. The default is linkifyjs.find.
     */
    findURLFn: (text: string) => string[];
    /**
     * Custom logic to be invoked when a specific link preview is dismissed
     */
    onLinkPreviewDismissed?: (linkPreview: LinkPreview) => void;
  };
  location: {
    /**
     * Allows for toggling the location addition.
     * By default, the feature is enabled but has to be enabled also on channel level config via shared_locations.
     */
    enabled: boolean;
    /** Function that provides a stable id for a device from which the location is shared */
    getDeviceId: () => string;
  };
  text: {
    /**
     * Enables text input. Enabled by default.
     */
    enabled: boolean;
    /**
     * Enable sending typing events which are used to display typing indicators. Enabled by default.
     */
    publishTypingEvents: boolean;
    /**
     * Initial text value if draft or existing message is not available.
     */
    defaultValue?: string;
    /**
     * The message text will be trimmed to maxLengthOnEdit on text change or suggestion selection.
     */
    maxLengthOnEdit?: number;
    /**
     * The message text will be trimmed to maxLengthOnSend during the final message composition before being sent.
     */
    maxLengthOnSend?: number;
  };
};
```

### Configuration Methods

**1. Global Configuration**

Use `client.setMessageComposerSetupFunction` to configure all `MessageComposer` instances:

```ts
client.setMessageComposerSetupFunction(({ composer }) => {
  composer.updateConfig({
    drafts: { enabled: true },
    attachments: {
      maxNumberOfFilesPerMessage: 5,
      acceptedFiles: ["image/*", "video/*"],
    },
  });
});
```

This applies to all existing and future `MessageComposer` instances (for example, for editing a message).

**2. Instance-Specific Configuration**

Configure a specific `MessageComposer` instance using `updateConfig`:

```ts
messageComposer.updateConfig({
  text: {
    enabled: true,
    publishTypingEvents: false,
  },
});
```

**3. Manager-Level Configuration**

Each manager can be configured individually through its properties:

```ts
// Configure TextComposer
messageComposer.textComposer.defaultValue = "Dear Customer Service";

// Configure AttachmentManager
messageComposer.attachmentManager.acceptedFiles = ["image/*", "video/*"];

// Toggle attachment upload progress (`localMetadata.uploadProgress` and `onProgress` for custom uploads)
messageComposer.updateConfig({ attachments: { trackUploadProgress: false } });

// Configure LinkPreviewsManager
messageComposer.linkPreviewsManager.enabled = true;

// General message composition parameters. Applies to drafts.
messageComposer.updateConfig({ drafts: enabled });
```

### Configuring Message ID Generation

You can provide a custom message ID generator by overriding `MessageComposer.generateId`:

```ts
messageComposer.generateId = customGenerator;
```


## Message Composer Context Type

Each `MessageComposer` has a `contextType` derived from `compositionContext`. Possible values: `channel`, `thread`, `legacy_thread`, or `message`. Use this to scope middlewares by context.

<admonition type="note">

Type `legacy_thread` is a simple message object extended with `legacyThreadId` property whose value equals to that message's `id`.

</admonition>

## Accessing the Corresponding MessageComposer instance

`useMessageComposer` returns the correct `MessageComposer` for the current context (channel, thread, message) and handles:

- Composing a new message in a channel
- Replying in a thread
- Editing an existing message
- Replying in a legacy thread that does not rely on Thread instance

Usage:

```tsx
import { useMessageComposer } from "stream-chat-react-native";

const MyComponent = () => {
  const messageComposer = useMessageComposer();
  const handleSend = async () => {
    // Access composer features
    const { localMessage, message, sendOptions } =
      await messageComposer.compose();
    // Handle the composed message
    // ...
  };

  return <button onClick={handleSend}>Send</button>;
};
```

Resolution order:

1. **Editing a message**: If a message is being edited, a new composer instance is created for that specific message
2. **Thread reply**: If in a thread context, it uses the thread's composer instance
3. **Legacy thread reply**: If replying without a `Thread` instance (legacy thread).
4. **Channel message**: Falls back to the channel's composer instance

It also:

- Registers the necessary subscriptions when the composer is mounted
- Cleans up subscriptions when the component unmounts

The hook caches composer instances for:

- Editing a message
- Replying in a legacy thread

This preserves composition state when returning to those contexts.

## Message Composer Utility Hooks

### useAttachmentManagerState

```tsx
const {
  attachments,
  availableUploadSlots,
  blockedUploadsCount,
  failedUploadsCount,
  isUploadEnabled,
  pendingUploadsCount,
  successfulUploadsCount,
  uploadsInProgressCount,
} = useAttachmentManagerState();
```

Use this hook when you need to:

- Access the attachment data
- Track attachment upload progress and status (`localMetadata.uploadProgress` is `0`–`100` while uploading when composer attachment config has `trackUploadProgress: true`; otherwise it stays `undefined`)
- Enable/disable UI elements based on upload state
- Display attachment counts and limits

### useAttachmentsForPreview

Reactively supplies currently composed message's:

- `attachments` array
- `location` object
- `poll` object

```tsx
const { attachments, location, poll } = useAttachmentsForPreview();
```

Use this hook when building previews for non-text message entities.

### useCanCreatePoll

```tsx
const canCreatePoll = useCanCreatePoll();
```

Use this hook to:

- Determine if a poll can be created with the current state
- Enable/disable the send button based on poll validity

### useMessageComposerHasSendableData

```tsx
const hasSendableData = useMessageComposerHasSendableData();
```

Use this hook to:

- Enable/disable the send button based on composer state



---

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

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/](https://getstream.io/chat/docs/sdk/react-native/v8/ui-components/message-input/composer/message-composer/).