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:

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

New in v8: The MessageComposer is a significant architectural change from v7. If you're upgrading, see the migration guide for details on how to update your code.

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

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:

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[];
    /**
     * Custom upload request (and possibly logic). For example upload to custom CDN.
     */
    doUploadRequest?: (
      file: FileLike,
    ) => Promise<{ file: string; thumb_url?: string }>;
  };
  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:

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:

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

3. Manager-Level Configuration

Each manager can be configured individually through its properties:

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

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

// 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:

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.

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

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:

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

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
  • Show upload progress indicators
  • 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
const { attachments, location, poll } = useAttachmentsForPreview();

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

useCanCreatePoll

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

const hasSendableData = useMessageComposerHasSendableData();

Use this hook to:

  • Enable/disable the send button based on composer state