Composer hooks
These hooks read the message composer and its parts: attachments, polls, commands and uploads. 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.
Most hooks here read the composer from useMessageComposerController(), so they need what it needs: Chat, and a Channel or a Thread.
useMessageComposerController
Returns the composer the component writes into.
useMessageComposerController(): MessageComposer- Returns, the first that applies:
- the composer a
MessageComposerControllerProviderabove supplies, such as one editing a message inline; - inside a
Thread,thread.messageComposer; channel.messageComposer.
- the composer a
- Reads: the composer, thread and channel contexts. It keeps the composer's event subscriptions running while the component is mounted. It subscribes to no store.
- Re-renders: when the supplied composer, the thread or the channel changes. Not when the composer's state changes: read its stores with
useStateStoreor the hooks below. - Needs:
Chat, and aChannelor aThread. Throws outside them, even below aMessageComposerControllerProvider.
Use it rather than channel.messageComposer: inside a thread it returns the thread's composer.
useMessageComposerControllerContext
Returns the composer a MessageComposerControllerProvider supplies.
useMessageComposerControllerContext(): MessageComposer | undefined- Returns: the supplied composer, or
undefinedwhen no provider supplies one. - Reads: the context of
MessageComposerControllerProvider. No store. - Re-renders: when the provider supplies another composer.
- Needs: nothing. Never throws.
Use it to tell a supplied composer apart from the thread's or channel's own, for example to show an "Editing" label.
useMessageComposerContext
Returns the settings and bindings of the MessageComposer around the component.
useMessageComposerContext(): MessageComposerContextValue- Returns:
textareaRef,onPasteandrecordingController: the bindings fromuseMessageComposerBindings.additionalTextareaProps,asyncMessagesMultiSendEnabled,audioRecordingEnabled,emojiSearchIndex,focus,hideSendButton,maxRows,minRows,parentandshouldSubmit: theMessageComposerprops of the same names.emojiSearchIndexfalls back to the one inWithComponents.
- Reads: the context
MessageComposerprovides. It holds UI settings, not the composition: the text, attachments and the rest are in the composer's stores. - Re-renders: when
MessageComposerprovides a new value. - Needs:
MessageComposer. Throws outside it.useIsWithinMessageComposerContext()returns whether the component is inside one, without throwing.
The context is on MessageComposerContext.
useMessageComposerSubmitFn
Returns the function that submits the composition.
useMessageComposerSubmitFn(): () => Promise<MessageComposerSubmitResult>- Returns: a function that calls
MessageComposer.update()while a message is being edited, andMessageComposer.send()otherwise. Its promise never rejects: it resolves to'sent','nothing-to-send'or'failed'. - Reads: whether a message is being edited, when it's called. No store.
- Re-renders: never by itself.
- Needs: what
useMessageComposerControllerneeds.
The default send button and the Enter key submit through it. Use it in your own submit controls so they submit the same way. What MessageComposer.send() and MessageComposer.update() do is on Message composer.
useMessageComposerBindings
Creates the bindings MessageComposer puts in its context.
useMessageComposerBindings(props: MessageComposerProps): UseMessageComposerBindingsParams- Returns:
textareaRef: a ref for the text input. It focuses the input on mount whenfocusis set.onPaste: a paste handler. It inserts pasted text, or uploads pasted files.recordingController: the voice recorder, set up fromaudioRecordingEnabled,audioRecordingConfigandasyncMessagesMultiSendEnabled.
- Reads: the composer from
useMessageComposerController(). - Re-renders: when the recorder's state changes.
- Needs: what
useMessageComposerControllerneeds.
You rarely need it. MessageComposer calls it, and its children read the result through useMessageComposerContext(). Call it only when you build a replacement for MessageComposer itself.
useAttachmentManagerState
Returns the attachments in the composer and what can still be added.
useAttachmentManagerState(): { attachments: LocalAttachment[]; attachmentsEnabled: boolean; availableUploadSlots: number; blockedUploadsCount: number; customCdn: boolean; failedUploadsCount: number; hasAvailableUploadSlots: boolean; isUploadEnabled: boolean; locationEnabled: boolean; maxNumberOfFilesPerMessage: number; pendingUploadsCount: number; pollsEnabled: boolean; successfulUploadsCount: number; uploadsInProgressCount: number }- Returns:
attachments: the attachments in the composition.- The counts of attachments in each upload state:
pendingUploadsCount,uploadsInProgressCount,successfulUploadsCount,failedUploadsCount, andblockedUploadsCountfor files the server's upload settings refused. availableUploadSlots: how many more files fit undermaxNumberOfFilesPerMessage.hasAvailableUploadSlotssays whether any do.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.attachmentsEnabled,customCdn,locationEnabledandpollsEnabled: the composer settings in effect.
- Reads:
attachmentManager.state, fieldattachments, andmessageComposer.configState. The counts and flags are worked out from those on each render. - Re-renders: when the attachments change, and when one of those settings changes.
- Needs: what
useMessageComposerControllerneeds.
Uploads and their limits are explained on Attachments and uploads.
useAttachmentsForPreview
Returns what the composer shows above the text input: attachments, a poll and a location.
useAttachmentsForPreview(): { attachments: LocalAttachment[]; location: StaticLocationPreview | LiveLocationPreview | null; poll: PollComposerState["data"] }- Returns:
attachments: the attachments in the composition.poll: the poll being composed, from the poll composer's form.location: the location to share, ornull.
- Reads:
attachmentManager.state(attachments),pollComposer.state(data) andlocationComposer.state(location). - Re-renders: when one of the three changes.
- Needs: what
useMessageComposerControllerneeds.
The Attachment previews recipe builds a custom preview list.
useCanCreatePoll
Says whether the poll being composed can be created.
useCanCreatePoll(): boolean- Returns:
truewhen the poll form is valid: it has a name, at least one option with text, a valid maximum number of votes, and no field errors. - Reads:
pollComposer.state, throughpollComposer.canCreatePoll. - Re-renders: when the answer changes. It checks again on every change to the poll form.
- Needs: what
useMessageComposerControllerneeds.
Use it to enable the create button of a custom poll form. Polls are on Polls.
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. - Re-renders: on every change to the composition.
- Needs: what
useMessageComposerControllerneeds.
The default send button is disabled while this is false. Sending with pending uploads explains how pending uploads count.
useMessageContentIsEmpty
Says whether the composition has no content.
useMessageContentIsEmpty(): boolean- Returns:
messageComposer.contentIsEmpty:truewhen there's no text, no attachment, no created poll and no location. A quoted message doesn't count as content. - Reads:
messageComposer.editingAuditState. - Re-renders: on every change to the composition.
- Needs: what
useMessageComposerControllerneeds.
Unlike useMessageComposerHasSendableData, it doesn't look at uploads or commands. An attachment still uploading makes the content non-empty, although it can't be sent yet.
useMessageComposerCommands
Returns the slash commands available in the composer, and whether each can be used now.
useMessageComposerCommands(): MessageComposerCommand[]- Returns: one
{ command, enabled }for each command the channel offers.commandhas the command'sname,descriptionandargs.enabledisfalsefor every command while a message is being edited, and for moderation commands while a message is quoted. - Reads: the composer channel's
configState, fieldavailableCommands, andmessageComposer.state, fieldseditedMessageandquotedMessage. - Re-renders: when the available commands change, and when an edit or a quote starts or ends.
- Needs: what
useMessageComposerControllerneeds.
import { useMessageComposerCommands } from "stream-chat-react";
export const CommandHints = () => {
const commands = useMessageComposerCommands();
return (
<ul>
{commands.map(({ command, enabled }) => (
<li aria-disabled={!enabled} key={command.name}>
/{command.name} {command.args}: {command.description}
</li>
))}
</ul>
);
};The server decides which commands a channel has. Commands are explained on Suggestions and Commands and Giphy.
useAttachmentUploadState
Returns whether an attachment of a message is uploading, and how far along it is.
useAttachmentUploadState(attachment?: Attachment | LocalAttachment | SharedLocationResponse): AttachmentUploadState- Returns:
{ isUploading, progress, uploadConfirmationPending }.isUploading:truewhile a request for the attachment is in flight.progress: 0 to 100 while uploading, when the size is known.undefinedotherwise.uploadConfirmationPending:trueonce every byte is sent but the server hasn't answered yet. Show an indeterminate indicator then: the bar would sit at 100% with nothing confirmed.
- Reads:
client.uploadManager.state, fielduploads: the record of the attachment's request, which exists only while the request runs. - Re-renders: when the request's progress or confirmation state changes.
- Needs:
Chat. Throws outside it.
It's meant for attachments on messages sent before their uploads finished. An attachment from the server never matches a request, so it always reports isUploading: false. See Sending with pending uploads.
useAttachmentsUploadState
The same as useAttachmentUploadState, for several attachments at once.
useAttachmentsUploadState(attachments: (Attachment | LocalAttachment | SharedLocationResponse | undefined)[]): AttachmentUploadState- Returns: one combined
{ isUploading, progress, uploadConfirmationPending }.isUploadingistruewhile any of the attachments uploads.progressis the average, once every upload in flight reports one.uploadConfirmationPendingistruewhen any upload waits for the server.
- Reads:
client.uploadManager.state, fielduploads. - Re-renders: when one of the requests changes.
- Needs:
Chat. Throws outside it.
Use it for one progress bar over a gallery of images.
Composer stores without a hook
Read these stores with useStateStore and your own selector, using the composer from useMessageComposerController():
| 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, or only whether it's on with useIsCooldownActive, which re-renders less often.
Best practices
- Read composer state through these hooks or
useStateStore. Don't copy it into React state. - Get the composer from
useMessageComposerController(), not fromchannel.messageComposer: inside a thread, it returns the thread's composer. - Submit through
useMessageComposerSubmitFn(), so your controls, the Enter key and the default send button submit the same way. - 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 {
useAttachmentManagerState,
useCooldownRemaining,
useMessageComposerHasSendableData,
} from "stream-chat-react";
export const ComposerStatus = () => {
const { pendingUploadsCount, uploadsInProgressCount } =
useAttachmentManagerState();
const canSend = useMessageComposerHasSendableData();
const cooldownRemaining = useCooldownRemaining();
return (
<div>
<div>Uploads waiting: {pendingUploadsCount + uploadsInProgressCount}</div>
<div>Can send: {String(canSend)}</div>
{cooldownRemaining > 0 && <div>Slow mode: {cooldownRemaining}s</div>}
</div>
);
};Send button
A send button for a custom composer. It submits like the default one, stays disabled while there's nothing to send, and counts down in slow mode:
import {
useCooldownRemaining,
useMessageComposerHasSendableData,
useMessageComposerSubmitFn,
} from "stream-chat-react";
export const CustomSendButton = () => {
const hasSendableData = useMessageComposerHasSendableData();
const cooldownRemaining = useCooldownRemaining();
const submit = useMessageComposerSubmitFn();
return (
<button
disabled={!hasSendableData || cooldownRemaining > 0}
onClick={() => void submit()}
type="button"
>
{cooldownRemaining > 0 ? `Wait ${cooldownRemaining}s` : "Send"}
</button>
);
};The Custom composer recipe puts controls like these together.