Info:
This is release candidate documentation for Stream Chat React Native SDK v10. For the latest stable version, see the latest version (v9).

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:
    1. while the user edits a message, the composer that edits it;
    2. inside a Thread, thread.messageComposer;
    3. channel.messageComposer.
  • Reads: the composer context Channel and Thread provide. 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 useStateStore or the hooks below.
  • Needs: a Channel or a Thread. 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 editing message. The provider takes the message's composer from client.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 with null clears, the quoted message of the composer useMessageComposer() returns.
  • Reads: the context Channel and Thread provide. No store.
  • Re-renders: never. The functions keep their identity.
  • Needs: a Channel or a Thread. 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 the send-links capability. Saving the edit of a bounced message sends it again as a new message.
    • pickFile(), pickAndUploadImageFromNativePicker(), takeAndUploadImage(mediaType?) and uploadNewFile(file): open the native pickers or the camera, and upload what the user picks into the composer. Images are compressed with compressImageQuality first.
    • openAttachmentPicker() and closeAttachmentPicker(): open and close the attachment picker.
    • startVoiceRecording(), stopVoiceRecording(), deleteVoiceRecording(), uploadVoiceRecording(sendOnComplete) and audioRecorderManager: the voice recorder.
    • inputBoxRef and setInputBoxRef: the ref of the text input.
    • The props set on Channel or Thread that configure the composer, such as audioRecordingEnabled, hasCommands, hasFilePicker, hasImagePicker, hasCameraPicker, messageInputFloating, additionalTextInputProps and compressImageQuality.
  • Reads: the context Channel and Thread provide. It holds UI settings and actions, not the composition: the text, attachments and the rest are in the composer's stores.
  • Re-renders: when hasCommands changes, 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 Channel or a Thread. 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, and blockedUploadsCount for files the server's upload settings refused.
    • availableUploadSlots: how many more files fit under the composer's maxNumberOfFilesPerMessage.
    • isUploadEnabled: whether the composer accepts a file now. It needs uploads turned on, a free slot, and the upload-file capability unless files go to your own CDN.
  • Reads: attachmentManager.state, field attachments. The counts and isUploadEnabled are 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 Channel or a Thread. 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's true when 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's attachments.pendingUploadsEnabled setting in messageComposer.configState.
  • Re-renders: on every change to the composition, and when the setting changes.
  • Needs: a Channel or a Thread. 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.pendingUploadsEnabled setting. A thread's composer answers for itself.
  • Reads: messageComposer.configState.
  • Re-renders: when the setting changes.
  • Needs: a Channel or a Thread. 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: true while a request for the attachment is in flight.
    • uploadProgress: 0 to 100 while uploading, when the size is known. undefined otherwise.

    An upload that ends after reaching 90% keeps reporting isUploading: true at 100 for 350 milliseconds, so a progress ring doesn't vanish halfway.

  • Reads: client.uploadManager.state, field uploads: 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() and closePicker(): open and close the picker's bottom sheet. useMessageInputContext() has openAttachmentPicker(), which also dismisses the keyboard and selects the image tab, and closeAttachmentPicker(), 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, numberOfAttachmentPickerImageColumns and numberOfAttachmentImagesToLoadPerCall: the Channel and Thread props of the same names.
  • Reads: the context Channel and Thread provide. No store.
  • Re-renders: whenever the Channel or Thread renders.
  • Needs: a Channel or a Thread. 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, or undefined while the picker is closed.
  • Reads: attachmentPickerStore.state from useAttachmentPickerContext().
  • Re-renders: when the selected tab changes.
  • Needs: a Channel or a Thread. 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 from channel.messageComposer: inside a thread, and during an edit, it returns another composer.
  • Send through sendMessage() from useMessageInputContext(), 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.