MessageComposer
MessageComposer renders the message input of a channel or a thread. The message being written lives in a MessageComposer object from stream-chat, such as channel.messageComposer. The component renders that object and calls it when the user types, attaches, records or sends. Message composer UI explains the parts it renders and which composer it writes into, and Message composer explains the object.
Best practices
- Render it inside a
Channelor aThread. It reads the channel, the composer and its settings from them, and throws outside both. - Configure the composer on
ChannelandThread, not onMessageComposer. Props such asaudioRecordingEnabled,hasCommandsandmessageInputFloatingreach the composer's parts and the message list throughMessageInputContext. Most of the propsMessageComposeraccepts don't, as the Props tables show. - Change what is sent in the state layer: transform the message with composer middleware, or replace the request with a
sendMessageRequesthandler, as Customizing requests shows. - Change the look with
WithComponents. Each part of the composer has an override key, listed on Composer UI components. - Declare
additionalTextInputPropsonce, at module scope or inuseMemo.MessageComposerre-renders when the object changes.
Basic usage
import type {
Channel as StreamChannel,
Thread as StreamThread,
} from "stream-chat";
import {
Channel,
MessageComposer,
MessageList,
Thread,
} from "stream-chat-react-native";
export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => (
<Channel audioRecordingEnabled channel={channel}>
<MessageList />
<MessageComposer />
</Channel>
);
export const ThreadScreen = ({ thread }: { thread: StreamThread }) => (
<Thread audioRecordingEnabled thread={thread}>
<MessageList />
<MessageComposer />
</Thread>
);The first composer writes into channel.messageComposer, the second into thread.messageComposer. Voice recording is turned on with a prop of Channel and of Thread, because neither passes its configuration to the other.
What it renders
MessageComposer renders the composer row: MessageComposerLeadingView with the attach button, then a box with MessageInputHeaderView (the previews), InputView (the command chip, the text input and the also-send-to-channel checkbox) and MessageInputTrailingView (the send button or its replacement), then MessageComposerTrailingView. While the user records a voice message, the recorder parts take the input's place. Each of these is an override key; Composer UI components lists them with their props, and Audio recorder covers the recorder.
It also renders:
- in place of everything above,
SendMessageDisallowedIndicator, once the channel's capabilities are known and the user lackssend-message, unless a message is being edited; - the default poll creation dialog,
CreatePollin aPollModal, whileshowPollCreationDialogistrue(see Poll components); - the
Inputoverride in place of the row, when you register one. It getsadditionalTextInputPropsandgetUsers(), which returns the users ofmembersandwatcherswithout duplicates.
When it mounts inside a Thread, it loads the thread's saved draft into the thread's composer, when drafts are turned on. When it unmounts, it closes the attachment picker. It doesn't save or clear the composer: see Drafts.
With messageInputFloating, the row floats over the message list, with a shadow, instead of sitting below it. The message list reads the same setting from MessageInputContext to leave room for it, so set it on Channel or Thread.
Props
All props are optional. Without a prop, MessageComposer uses the value it reads from its context or computes itself, which the Default column names.
| Prop | Type | Default | Description |
|---|---|---|---|
additionalTextInputProps |
TextInputProps |
the Channel or Thread prop |
Props for the React Native TextInput, also passed to the Input override. They're spread after the SDK's own, so onChangeText, onSelectionChange, value and style replace the SDK's handling and styles. A placeholder replaces "Send a message", a command's placeholder and the slow mode placeholder. numberOfLines sets how many lines the input grows to, 5 by default. |
TextInputComponent |
TextInputOverrideComponent |
React Native's TextInput |
The text input component. It gets the same props as the default and must forward its ref. |
closeAttachmentPicker |
() => void |
from MessageInputContext |
Called when MessageComposer unmounts. |
showPollCreationDialog |
boolean |
from MessageInputContext |
Whether the default poll creation dialog shows. |
closePollCreationDialog |
() => void |
from MessageInputContext |
Closes the default poll creation dialog. Passed to CreatePoll and to PollModal's onRequestClose. |
createPollOptionGap |
number |
none | Passed to CreatePoll: the gap between the options of the poll form, 8 when unset. MessageComposer doesn't read the Channel or Thread prop of the same name from MessageInputContext. |
sendMessage |
() => Promise<void> |
from MessageInputContext |
Passed to CreatePoll, which calls it after creating the poll. The send button doesn't use this prop: it reads sendMessage from MessageInputContext. |
messageInputFloating |
boolean |
from MessageInputContext, false |
Floats the row over the message list. See What it renders. |
messageInputHeightStore |
MessageInputHeightStore |
from MessageInputContext |
The store the row writes its height into. The message list reads the context's store, so replacing it here leaves the list without the height. |
asyncMessagesLockDistance |
number |
from MessageInputContext, 50 |
Here only the lock indicator's slide animation reads it. The record button's gesture reads the context's value, so set it on Channel or Thread. |
asyncMessagesSlideToCancelDistance |
number |
from MessageInputContext, 75 |
Here only the fade of "Slide to Cancel" reads it. The record button's gesture reads the context's value, so set it on Channel or Thread. |
inputBoxRef |
React.RefObject<InputBoxRef | null> |
from MessageInputContext |
Read to style the focused input box and to move screen reader focus to the input on mount. The text input fills the context's ref, not one passed here. |
channel |
Channel |
the channel of Channel or Thread |
Read only for getUsers() of the Input override, when members or watchers isn't set. |
members |
MembersState['members'] |
channel.state members |
The members getUsers() returns, for the Input override. |
watchers |
ChannelWatchState['watchers'] |
channel.state watchers |
The watchers getUsers() returns, for the Input override. |
isKeyboardVisible |
boolean |
useKeyboardVisibility() |
Whether the keyboard is open. The row's bottom padding and its place over an open attachment picker depend on it. Meant for the SDK's own use. |
isRecordingStateIdle |
boolean |
the recorder's state | Whether no recording is in progress. While it's false, AudioRecorder takes the input's place. Meant for the SDK's own use. |
micLocked |
boolean |
the recorder's state | Whether the recording is locked. Shows AudioRecordingInProgress above the input. Meant for the SDK's own use. |
recordingStatus |
string |
the recorder's state | "idle", "recording" or "stopped". "stopped" shows AudioRecordingPreview above the input. Meant for the SDK's own use. |
Props that have no effect
MessageComposer accepts these props, but none of its parts reads them from MessageComposer. The parts read the values from their contexts instead. Set the configuration ones on Channel or Thread.
| Prop | Type | Where the parts read it |
|---|---|---|
audioRecordingEnabled |
boolean |
MessageInputContext, set on Channel or Thread |
audioRecordingSendOnComplete |
boolean |
MessageInputContext, set on Channel or Thread |
asyncMessagesMinimumPressDuration |
number |
MessageInputContext, set on Channel or Thread |
compressImageQuality |
number |
MessageInputContext, set on Channel or Thread |
openPollCreationDialog |
({ sendMessage }: Pick<LocalMessageInputContext, 'sendMessage'>) => void |
MessageInputContext, set on Channel or Thread |
audioRecorderManager |
AudioRecorderManager |
MessageInputContext |
uploadNewFile |
(file: File) => Promise<void> |
MessageInputContext |
clearEditingState |
() => void |
MessageComposerAPIContext |
editing |
boolean |
the composer's editedMessage |
isOnline |
boolean |
useSettledWSConnectionHealth() |
t |
StreamTFunction |
TranslationContext |
A change to audioRecordingEnabled, audioRecordingSendOnComplete, asyncMessagesMinimumPressDuration, openPollCreationDialog, editing, isOnline or t still re-renders MessageComposer.
Examples
Change the text input
This composer capitalizes sentences and shows a placeholder of its own. The props are declared once, at module scope:
import type { TextInputProps } from "react-native";
import type { Channel as StreamChannel } from "stream-chat";
import {
Channel,
MessageComposer,
MessageList,
} from "stream-chat-react-native";
const textInputProps: TextInputProps = {
autoCapitalize: "sentences",
placeholder: "Write to the team",
};
export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => (
<Channel channel={channel}>
<MessageList />
<MessageComposer additionalTextInputProps={textInputProps} />
</Channel>
);The placeholder replaces the command and slow mode placeholders too. To keep them, set additionalTextInputProps without a placeholder. To set the same props for every composer under a Channel or Thread, pass additionalTextInputProps to it instead.
Focus the input
inputBoxRef in MessageInputContext holds the text input. A part rendered inside the composer, or anywhere under the Channel or Thread, focuses it:
import { Pressable, Text } from "react-native";
import { useMessageInputContext } from "stream-chat-react-native";
export const FocusComposerButton = () => {
const { inputBoxRef } = useMessageInputContext();
return (
<Pressable onPress={() => inputBoxRef.current?.focus()}>
<Text>Reply</Text>
</Pressable>
);
};Hooks
useMessageComposer: the composer a component writes into.useMessageInputContext: the composer's settings, the send function, the pickers and the recorder. The values are on MessageInputContext.useMessageComposerHasSendableData: whether the send button is enabled.
Composer hooks lists the rest.