# Composer hooks

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

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](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/message-composer/), and its API on [MessageComposer API](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/messagecomposer-api/). How the default composer UI is built is on [Message composer UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/message-composer-ui/), and how to read state in components on [Reading state in components](https://getstream.io/chat/docs/sdk/react-native/v10/ui/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.

```ts
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.

```ts
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](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/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.

```ts
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](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message-composer/messageinputcontext/).

## useAttachmentManagerState

Returns the attachments in the composer and the counts of their uploads.

```ts
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](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/message-composer/#attachments-and-uploads).

## useMessageComposerHasSendableData

Says whether the composition can be sent.

```ts
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](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/sending-with-pending-uploads/) explains how pending uploads count.

## usePendingUploadsEnabled

Says whether the composer sends a message before its uploads finish.

```ts
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](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/sending-with-pending-uploads/#turning-it-on-or-off).

## usePendingAttachmentUpload

Returns whether an attachment of a sent message is still uploading, and how far along it is.

```ts
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](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/sending-with-pending-uploads/#reading-the-upload-state).

## useAttachmentPickerContext

Returns the attachment picker's settings and controls.

```ts
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](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message-composer/attachment-picker/).

## useAttachmentPickerState

Returns the attachment picker's selected tab.

```ts
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`.

```ts
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](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message-composer/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](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/messagecomposer-api/). The cooldown of slow mode is on the channel, not the composer: read the seconds left with [`useCooldownRemaining`](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/channel-and-thread-hooks/#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](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/composer-middleware/). Hooks only read it.

## Examples

### Composer status

```tsx
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:

```tsx
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](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/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](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/composer/custom-composer/) UI cookbook puts controls like these together.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/composer-hooks/](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/composer-hooks/).