Composer hooks
These hooks read the message composer and its parts: attachments, uploads and the attachment picker. Use them to build composer UI on the same MessageComposer the default components write into.
The composer is explained on Message composer, and its API on MessageComposer API. How the default composer UI is built is on Message composer UI, and how to read state in components on Reading state in components.
Each entry gives the signature, then:
- Returns: what the hook gives back.
- Reads: the store or the context it reads.
- Re-renders: what makes the component render again.
- Needs: where the component must sit, and what the hook does outside that place.
Channel and Thread provide the composer contexts to everything they render, not only to MessageComposer. Most hooks here read the composer from useMessageComposer(), so they need a Channel or a Thread, and throw outside both.
useMessageComposer
Returns the composer the component writes into.
useMessageComposer(): MessageComposer- Returns, the first that applies:
- while the user edits a message, the composer that edits it;
- inside a
Thread,thread.messageComposer; channel.messageComposer.
- Reads: the composer context
ChannelandThreadprovide. The provider keeps the composer's event subscriptions running. It subscribes to no store. - Re-renders: when an edit starts or ends, and when the thread or the channel changes. Not when the composer's state changes: read its stores with
useStateStoreor the hooks below. - Needs: a
Channelor aThread. Throws outside both.
Use it rather than channel.messageComposer: inside a thread it returns the thread's composer, and during an edit the edit composer.
useMessageComposerAPIContext
Returns the functions that start and end an edit, and set the quoted message.
useMessageComposerAPIContext(): MessageComposerAPIContextValue- Returns:
setEditingState(message): starts editingmessage. The provider takes the message's composer fromclient.messageComposerCache, or creates one, and starts it from the message.useMessageComposer()returns it until the edit ends. Called without a message, it ends the edit.clearEditingState(): ends the edit without saving.setQuotedMessage(message): sets, or withnullclears, the quoted message of the composeruseMessageComposer()returns.
- Reads: the context
ChannelandThreadprovide. No store. - Re-renders: never. The functions keep their identity.
- Needs: a
Channelor aThread. Throws outside both.
An edit also ends when the edit composer's editedMessage is cleared, which saving it does. Sending, editing and deleting shows the edit flow.
useMessageInputContext
Returns the settings of the composer UI, and the functions that send, pick files and record audio.
useMessageInputContext(): MessageInputContextValue- Returns:
sendMessage(): sends the composition, or saves the edit while a message is being edited. It clears the input first, and puts the text back when there was nothing to send. It shows an alert and sends nothing when the text has a link and the user lacks thesend-linkscapability. Saving the edit of a bounced message sends it again as a new message.pickFile(),pickAndUploadImageFromNativePicker(),takeAndUploadImage(mediaType?)anduploadNewFile(file): open the native pickers or the camera, and upload what the user picks into the composer. Images are compressed withcompressImageQualityfirst.openAttachmentPicker()andcloseAttachmentPicker(): open and close the attachment picker.startVoiceRecording(),stopVoiceRecording(),deleteVoiceRecording(),uploadVoiceRecording(sendOnComplete)andaudioRecorderManager: the voice recorder.inputBoxRefandsetInputBoxRef: the ref of the text input.- The props set on
ChannelorThreadthat configure the composer, such asaudioRecordingEnabled,hasCommands,hasFilePicker,hasImagePicker,hasCameraPicker,messageInputFloating,additionalTextInputPropsandcompressImageQuality.
- Reads: the context
ChannelandThreadprovide. It holds UI settings and actions, not the composition: the text, attachments and the rest are in the composer's stores. - Re-renders: when
hasCommandschanges, and when the poll creation dialog opens or closes. A change to another of those props after mount doesn't provide a new value. - Needs: a
Channelor aThread. Throws outside both.
The values are listed on MessageInputContext.
useAttachmentManagerState
Returns the attachments in the composer and the counts of their uploads.
useAttachmentManagerState(): {
attachments: LocalAttachment[];
availableUploadSlots: number;
blockedUploadsCount: number;
failedUploadsCount: number;
isUploadEnabled: boolean;
pendingUploadsCount: number;
successfulUploadsCount: number;
uploadsInProgressCount: number;
}- Returns:
attachments: the attachments in the composition.- The number of attachments in each upload state:
pendingUploadsCount,uploadsInProgressCount,successfulUploadsCount,failedUploadsCount, andblockedUploadsCountfor files the server's upload settings refused. availableUploadSlots: how many more files fit under the composer'smaxNumberOfFilesPerMessage.isUploadEnabled: whether the composer accepts a file now. It needs uploads turned on, a free slot, and theupload-filecapability unless files go to your own CDN.
- Reads:
attachmentManager.state, fieldattachments. The counts andisUploadEnabledare read from the attachment manager on each render. - Re-renders: when the attachments change. A change to the composer's settings or the channel's capabilities alone doesn't re-render it.
- Needs: a
Channelor aThread. Throws outside both.
Uploads and their limits are explained on Message composer.
useMessageComposerHasSendableData
Says whether the composition can be sent.
useMessageComposerHasSendableData(): boolean- Returns:
messageComposer.hasSendableData. It'struewhen the composition has text, a sendable attachment, a created poll or a location, and the command in it, if any, is ready to send.- By default, an attachment counts once its upload finishes, and text and attachments can't be sent while an upload is in progress.
- When sending with pending uploads is on, attachments still uploading, and failed ones, count too. Blocked ones never do.
- Reads:
messageComposer.editingAuditState, which changes on every change to the composition, and the composer'sattachments.pendingUploadsEnabledsetting inmessageComposer.configState. - Re-renders: on every change to the composition, and when the setting changes.
- Needs: a
Channelor aThread. Throws outside both.
The default send button is disabled while this is false. Sending with pending uploads explains how pending uploads count.
usePendingUploadsEnabled
Says whether the composer sends a message before its uploads finish.
usePendingUploadsEnabled(): boolean- Returns: the composer's
attachments.pendingUploadsEnabledsetting. A thread's composer answers for itself. - Reads:
messageComposer.configState. - Re-renders: when the setting changes.
- Needs: a
Channelor aThread. Throws outside both.
Chat turns the setting on when enableOfflineSupport is set, unless a value is already set in client.config. See Sending with pending uploads.
usePendingAttachmentUpload
Returns whether an attachment of a sent message is still uploading, and how far along it is.
usePendingAttachmentUpload(localId: string | undefined): {
isUploading: boolean;
uploadProgress: number | undefined;
}-
Returns:
isUploading:truewhile a request for the attachment is in flight.uploadProgress: 0 to 100 while uploading, when the size is known.undefinedotherwise.
An upload that ends after reaching 90% keeps reporting
isUploading: trueat 100 for 350 milliseconds, so a progress ring doesn't vanish halfway. -
Reads:
client.uploadManager.state, fielduploads: the record of the attachment's request, keyed by the attachment's local ID. The record exists only while the request runs. -
Re-renders: when the request's progress changes, when it ends, and once more when the 350 ms are over.
-
Needs:
Chat. Throws outside it.
It's meant for attachments on messages sent before their uploads finished. An attachment from the server has no request, so it always reports isUploading: false. AttachmentUploadIndicator receives the local ID and calls it: see Sending with pending uploads.
useAttachmentPickerContext
Returns the attachment picker's settings and controls.
useAttachmentPickerContext(): AttachmentPickerContextValue- Returns:
openPicker()andclosePicker(): open and close the picker's bottom sheet.useMessageInputContext()hasopenAttachmentPicker(), which also dismisses the keyboard and selects the image tab, andcloseAttachmentPicker(), which also clears the selected tab.attachmentPickerStore: the store of the picker's selected tab.bottomSheetRef: the ref of the picker's bottom sheet.bottomInset,topInset,disableAttachmentPicker,shouldRenderAttachmentPicker,attachmentPickerBottomSheetHeight,attachmentSelectionBarHeight,numberOfAttachmentPickerImageColumnsandnumberOfAttachmentImagesToLoadPerCall: theChannelandThreadprops of the same names.
- Reads: the context
ChannelandThreadprovide. No store. - Re-renders: whenever the
ChannelorThreadrenders. - Needs: a
Channelor aThread. Throws outside both.
The picker is on Attachment picker.
useAttachmentPickerState
Returns the attachment picker's selected tab.
useAttachmentPickerState(): { selectedPicker: SelectedPickerType }- Returns:
{ selectedPicker }:'images','files','camera-photo','camera-video','polls','commands', a name of your own, orundefinedwhile the picker is closed. - Reads:
attachmentPickerStore.statefromuseAttachmentPickerContext(). - Re-renders: when the selected tab changes.
- Needs: a
Channelor aThread. Throws outside both.
useAudioRecorder
Creates the voice recorder's functions for an AudioRecorderManager.
useAudioRecorder(params: {
audioRecorderManager: AudioRecorderManager;
sendMessage: () => Promise<void>;
}): {
deleteVoiceRecording: () => Promise<void>;
startVoiceRecording: () => Promise<boolean | undefined>;
stopVoiceRecording: (withDelete?: boolean) => Promise<void>;
uploadVoiceRecording: (sendOnComplete: boolean) => Promise<void>;
}Channel and Thread call it already, and useMessageInputContext() returns its functions together with the audioRecorderManager they act on. Read them from there. Calling the hook again with the same manager adds a second recorder that stops the recording when its component unmounts. The recorder UI is on Audio recorder.
Composer stores without a hook
Read these stores with useStateStore and your own selector, using the composer from useMessageComposer():
| To read | Store |
|---|---|
| The quoted message, the message being edited, the poll ID | messageComposer.state |
| The text, mentions, the active command and the suggestions | textComposer.state |
| The poll form and its errors | pollComposer.state |
| The location to share | locationComposer.state |
| Link previews | linkPreviewsManager.state |
| Custom data | customDataManager.state |
| The composer's settings in effect | messageComposer.configState |
The fields of each store are on MessageComposer API. The cooldown of slow mode is on the channel, not the composer: read the seconds left with useCooldownRemaining.
Best practices
- Read composer state through these hooks or
useStateStore. Don't copy it into React state. - Get the composer from
useMessageComposer(), not fromchannel.messageComposer: inside a thread, and during an edit, it returns another composer. - Send through
sendMessage()fromuseMessageInputContext(), so your controls send the way the default send button does. - Check whether there's something to send with
useMessageComposerHasSendableData(), so every control agrees. - Change what the composer does in the state layer: its settings in
client.config, and composer middleware. Hooks only read it.
Examples
Composer status
import { Text, View } from "react-native";
import {
useAttachmentManagerState,
useCooldownRemaining,
useMessageComposerHasSendableData,
} from "stream-chat-react-native";
export const ComposerStatus = () => {
const { pendingUploadsCount, uploadsInProgressCount } =
useAttachmentManagerState();
const canSend = useMessageComposerHasSendableData();
const cooldownRemaining = useCooldownRemaining();
return (
<View>
<Text>
Uploads waiting: {pendingUploadsCount + uploadsInProgressCount}
</Text>
<Text>Can send: {String(canSend)}</Text>
{cooldownRemaining > 0 && <Text>Slow mode: {cooldownRemaining}s</Text>}
</View>
);
};Send button
A send button for a custom composer. It sends like the default one, stays disabled while there's nothing to send, and counts down in slow mode:
import { Pressable, Text } from "react-native";
import {
useCooldownRemaining,
useMessageComposerHasSendableData,
useMessageInputContext,
} from "stream-chat-react-native";
export const CustomSendButton = () => {
const hasSendableData = useMessageComposerHasSendableData();
const cooldownRemaining = useCooldownRemaining();
const { sendMessage } = useMessageInputContext();
const disabled = !hasSendableData || cooldownRemaining > 0;
return (
<Pressable
accessibilityRole="button"
accessibilityState={{ disabled }}
disabled={disabled}
onPress={() => void sendMessage()}
>
<Text>
{cooldownRemaining > 0 ? `Wait ${cooldownRemaining}s` : "Send"}
</Text>
</Pressable>
);
};Pass it to WithComponents as SendButton, as Overriding components describes. In slow mode the default composer shows CooldownTimer in place of SendButton, so the countdown shows only in a composer of your own. The Custom composer UI cookbook puts controls like these together.