# Thread

`Thread` gives one thread to the components inside it. It renders its children with the contexts a message list and a composer need, the same ones `Channel` provides, but bound to the thread's replies and composer. Render it next to `Channel`, on a screen of its own, not inside it. [Thread UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/thread-ui/#the-thread-component) explains what it does, and [Replies and threads](https://getstream.io/chat/docs/sdk/react-native/features/messaging/replies-and-threads/) covers the feature.

## Best practices

- Get the thread from the client, never from `new Thread()`. `onThreadSelect` on `MessageList`, `MessageFlashList` and `ThreadList` gives you the thread the user picked, and `client.threads.ensure({ channel, parentMessage })` returns the thread for a message, creating it when needed. The client keeps one instance per thread and sends it its events.
- Compose the screen from children. `Thread` renders no message list or composer of its own: render `MessageList` or `MessageFlashList`, and `MessageComposer`, inside it.
- Pass `Thread` the same configuration you pass `Channel`, such as `keyboardVerticalOffset`, `topInset`, `audioRecordingEnabled` and `messageActions`. A thread doesn't inherit it from a channel.
- Clear your app's record of the open thread in `onThreadDismount`, for example when the user navigates back.
- A `Thread` needs no `Channel` around it. Inside it, `useChannel()` returns the thread's channel.

## Basic usage

The channel screen opens a thread when the user taps a message's replies, and the thread screen renders it:

```tsx title="src/ChatScreens.tsx"
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,
  openThread,
}: {
  channel: StreamChannel;
  openThread: (thread: StreamThread) => void;
}) => (
  <Channel channel={channel} keyboardVerticalOffset={0}>
    <MessageList onThreadSelect={openThread} />
    <MessageComposer />
  </Channel>
);

export const ThreadScreen = ({
  onClose,
  thread,
}: {
  onClose: () => void;
  thread: StreamThread;
}) => (
  <Thread keyboardVerticalOffset={0} onThreadDismount={onClose} thread={thread}>
    <MessageList />
    <MessageComposer />
  </Thread>
);
```

`MessageList` renders the parent message above the replies, and `MessageComposer` writes into `thread.messageComposer`. [Moving between channels, threads and messages](https://getstream.io/chat/docs/sdk/react-native/ui/layout/moving-between-channels-threads-and-messages/) passes the thread from one screen to the other, and opens a thread at a reply.

## What it does

While it's mounted, `Thread`:

- loads the first page of replies with `paginator.toTail()` on `thread.messagePaginator`, unless the paginator already holds some. A thread from the thread list arrives with its latest replies;
- shows the newest replies again when the thread is opened on an older page of replies, for example after a jump to a reply, unless a jump to a reply is in flight or has landed. The newest page is usually still loaded, so this needs no request. `useJumpToLatestOnOpen()` does it;
- reloads the thread with `Thread.reload()` while its state is stale;
- watches the thread's channel once when nobody has loaded it, so the user's capabilities in the channel reach the message menu, the reactions and the composer;
- activates the thread with `Thread.activate()`, so it receives its events even when the thread list doesn't hold it, and marks new replies read as they arrive. Opening a thread doesn't mark its channel read;
- routes its notifications to its own notification host.

When it unmounts, it releases the activation and calls `onThreadDismount`. When the first page of replies fails to load, it renders `LoadingErrorIndicator`, "Error loading replies...", in place of its children, and tapping it loads the replies again.

## UI customization

`Thread` has no override key. Change the screen by changing its children. Inside it, the message list and the composer take the same override keys as in a channel. To give the thread overrides of its own, wrap its children in `WithComponents`:

```tsx title="src/CompactThread.tsx"
import type { Thread as StreamThread } from "stream-chat";
import {
  MessageComposer,
  MessageList,
  Thread,
  WithComponents,
} from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";

const NoTimestamp = () => null;

const threadOverrides: ComponentOverrides = { MessageTimestamp: NoTimestamp };

export const CompactThread = ({ thread }: { thread: StreamThread }) => (
  <Thread thread={thread}>
    <WithComponents overrides={threadOverrides}>
      <MessageList />
      <MessageComposer />
    </WithComponents>
  </Thread>
);
```

The parent message isn't a child you place. The message list renders it at the older end of the list as `ThreadFooterComponent`: the parent message, the reply count, and `InlineLoadingMoreThreadIndicator` while older replies load. To change it, pass your own component as `FooterComponent` on `MessageList` or `HeaderComponent` on `MessageFlashList`; [Replacing the parent message area](https://getstream.io/chat/docs/sdk/react-native/features/messaging/replies-and-threads/#replacing-the-parent-message-area) shows one.

### ThreadFooterComponent props

`ThreadFooterComponent` reads the thread from `ThreadContext` and the parent message and reply count from `thread.state`, so it updates when they change. It renders nothing until the thread has its parent message. The parent message renders like any other message, so it shows its delivery status when it's the user's own.

| Prop                        | Type      | Default | Description                                                                                                   |
| --------------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `parentMessagePreventPress` | `boolean` | `true`  | Whether tapping and long-pressing the parent message do nothing. `false` makes it pressable like any message. |

`InlineLoadingMoreThreadIndicator` takes no props. It shows a spinner while `thread.messagePaginator` loads.

## Props

| Prop                           | Type         | Default               | Description                                                                                                                                                              |
| ------------------------------ | ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `thread`                       | `Thread`     | required              | The thread to render, a `Thread` from `stream-chat`. Get it from `onThreadSelect`, `client.threads.ensure()` or `client.threads.get()`.                                  |
| `children`                     | `ReactNode`  | none                  | The screen's content: usually `MessageList` or `MessageFlashList`, and `MessageComposer`.                                                                                |
| `allowThreadMessagesInChannel` | `boolean`    | `true`                | Shows the "Also send to channel" checkbox in the composer. `false` hides it.                                                                                             |
| `onThreadDismount`             | `() => void` | none                  | Called when `Thread` unmounts. Only the function passed on the first render is called.                                                                                   |
| `notificationHostId`           | `string`     | the thread's own host | The notification host the thread's notifications are routed to. By default, a host derived from the thread's id, which `getThreadNotificationHostId(thread.id)` returns. |

### Configuration shared with Channel

`Thread` also takes the configuration props of `Channel`, with the same types and defaults. They configure the parts inside it, and only those: a thread screen doesn't inherit the props of a channel screen. Their descriptions are on [Channel](https://getstream.io/chat/docs/sdk/react-native/component-reference/channel/channel/#props). The props that only `Channel` takes, `channel`, `initializeOnMount`, `markReadOnMount` and `initialScrollToFirstUnreadMessage`, are about loading a channel; `Thread` loads its replies itself. `Channel` also takes `client` and `t`, which `Thread` doesn't.

#### Messages

These reach `MessagesContext`, which the messages read.

| Prop                             | Type                                                                            | Default                                                                  |
| -------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `additionalPressableProps`       | `Omit<PressableProps, 'style'>`                                                 | none                                                                     |
| `customMessageSwipeAction`       | `({ channel, message, }: { channel: Channel; message: LocalMessage; }) => void` | none                                                                     |
| `disableTypingIndicator`         | `boolean`                                                                       | `false`                                                                  |
| `dismissKeyboardOnMessageTouch`  | `boolean`                                                                       | `true`                                                                   |
| `enableSwipeToReply`             | `boolean`                                                                       | `true`                                                                   |
| `FlatList`                       | `typeof NativeHandlers.FlatList \| undefined`                                   | the registered `FlatList`                                                |
| `forceAlignMessages`             | `Alignment \| boolean`                                                          | none                                                                     |
| `getMessageGroupStyle`           | `(params: MessageGroupStylesParams) => GroupStyle[]`                            | the SDK's grouping                                                       |
| `giphyVersion`                   | `keyof NonNullable<Attachment['giphy']>`                                        | `"fixed_height"`                                                         |
| `handleBan`                      | `(message: LocalMessage) => Promise<void>`                                      | none                                                                     |
| `handleBlockUser`                | `(user: LocalMessage['user']) => Promise<void>`                                 | none                                                                     |
| `handleCopy`                     | `(message: LocalMessage) => Promise<void>`                                      | none                                                                     |
| `handleDelete`                   | `(message: LocalMessage) => Promise<void>`                                      | none                                                                     |
| `handleDeleteForMe`              | `(message: LocalMessage) => Promise<void>`                                      | none                                                                     |
| `handleEdit`                     | `(message: LocalMessage) => void`                                               | none                                                                     |
| `handleFlag`                     | `(message: LocalMessage) => Promise<void>`                                      | none                                                                     |
| `handleMarkUnread`               | `(message: LocalMessage) => Promise<void>`                                      | none                                                                     |
| `handleMute`                     | `(message: LocalMessage) => Promise<void>`                                      | none                                                                     |
| `handlePinMessage`               | `((message: LocalMessage) => MessageActionType) \| null`                        | none                                                                     |
| `handleQuotedReply`              | `(message: LocalMessage) => Promise<void>`                                      | none                                                                     |
| `handleReaction`                 | `(message: LocalMessage, reactionType: string) => Promise<void>`                | none                                                                     |
| `handleRetry`                    | `(message: LocalMessage) => Promise<void>`                                      | none                                                                     |
| `handleThreadReply`              | `(message: LocalMessage) => Promise<void>`                                      | none                                                                     |
| `hasCreatePoll`                  | `boolean`                                                                       | whether polls are on for the channel                                     |
| `isAttachmentEqual`              | `(prevAttachment: Attachment, nextAttachment: Attachment) => boolean`           | none                                                                     |
| `markdownRules`                  | `MarkdownRules`                                                                 | none                                                                     |
| `messageActions`                 | `(param: MessageActionsParams) => MessageActionType[]`                          | the SDK's actions                                                        |
| `messageContentOrder`            | `MessageContentType[]`                                                          | quoted reply, gallery, files, poll, AI text, attachments, location, text |
| `messageOverlayTargetId`         | `string`                                                                        | `"@stream-io/message-root"`                                              |
| `messageSwipeToReplyHitSlop`     | `ViewProps['hitSlop']`                                                          | `{ left: width, right: width }`, with the window's width                 |
| `messageTextNumberOfLines`       | `number`                                                                        | none                                                                     |
| `myMessageTheme`                 | `DeepPartial<Theme>`                                                            | none                                                                     |
| `onAlsoSentToChannelHeaderPress` | `(payload: AlsoSentToChannelHeaderPressPayload) => void`                        | none                                                                     |
| `onLongPressMessage`             | `(payload: MessagePressableHandlerPayload) => void`                             | none                                                                     |
| `onPressInMessage`               | `(payload: MessagePressableHandlerPayload) => void`                             | none                                                                     |
| `onPressMessage`                 | `(payload: MessagePressableHandlerPayload) => void`                             | none                                                                     |
| `reactionListPosition`           | `'top' \| 'bottom'`                                                             | `"top"`                                                                  |
| `reactionListType`               | `'clustered' \| 'segmented'`                                                    | `"clustered"`                                                            |
| `selectReaction`                 | `(message: LocalMessage) => (reactionType: string) => Promise<void>`            | none                                                                     |
| `shouldShowUnreadUnderlay`       | `boolean`                                                                       | `true`                                                                   |
| `supportedReactions`             | `ReactionData[]`                                                                | the SDK's reactions                                                      |
| `urlPreviewType`                 | `'compact' \| 'full'`                                                           | `"full"`                                                                 |

#### Composer

These reach `MessageInputContext`. See [MessageInputContext](https://getstream.io/chat/docs/sdk/react-native/component-reference/message-composer/messageinputcontext/).

| Prop                                 | Type                                                                       | Default                                               |
| ------------------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------- |
| `additionalTextInputProps`           | `TextInputProps`                                                           | none                                                  |
| `asyncMessagesLockDistance`          | `number`                                                                   | `50`                                                  |
| `asyncMessagesMinimumPressDuration`  | `number`                                                                   | `500`                                                 |
| `asyncMessagesSlideToCancelDistance` | `number`                                                                   | `75`                                                  |
| `attachmentPickerBottomSheetHeight`  | `number`                                                                   | `333`, or `72` with `disableAttachmentPicker`         |
| `attachmentSelectionBarHeight`       | `number`                                                                   | `72`                                                  |
| `audioRecordingEnabled`              | `boolean`                                                                  | `false`                                               |
| `audioRecordingSendOnComplete`       | `boolean`                                                                  | `false`                                               |
| `compressImageQuality`               | `number`                                                                   | none                                                  |
| `createPollOptionGap`                | `number`                                                                   | `8`                                                   |
| `focusInputOnPickerClose`            | `boolean`                                                                  | `true`                                                |
| `handleAttachButtonPress`            | `() => void`                                                               | none                                                  |
| `hasCameraPicker`                    | `boolean`                                                                  | whether an image picker is installed                  |
| `hasCommands`                        | `boolean`                                                                  | whether the channel type has commands                 |
| `hasFilePicker`                      | `boolean`                                                                  | whether a document picker is installed                |
| `hasImagePicker`                     | `boolean`                                                                  | whether an image picker or media library is installed |
| `messageInputFloating`               | `boolean`                                                                  | `false`                                               |
| `openPollCreationDialog`             | `({ sendMessage }: Pick<LocalMessageInputContext, 'sendMessage'>) => void` | none                                                  |
| `setInputRef`                        | `(ref: TextInputRef \| null) => void`                                      | none                                                  |

`hasCreatePoll` has no effect in a thread: the attachment picker offers no poll button there.

#### Attachment picker

These reach `AttachmentPickerContext`. See [Attachment picker](https://getstream.io/chat/docs/sdk/react-native/component-reference/message-composer/attachment-picker/).

| Prop                                    | Type      | Default                        |
| --------------------------------------- | --------- | ------------------------------ |
| `bottomInset`                           | `number`  | `0`                            |
| `disableAttachmentPicker`               | `boolean` | `true` without a media library |
| `numberOfAttachmentImagesToLoadPerCall` | `number`  | `25`                           |
| `numberOfAttachmentPickerImageColumns`  | `number`  | `3`                            |
| `shouldRenderAttachmentPicker`          | `boolean` | `true`                         |
| `topInset`                              | `number`  | `0`                            |

#### Message list

These reach `ChannelContext`, which the message list reads.

| Prop                                  | Type      | Default  |
| ------------------------------------- | --------- | -------- |
| `allowDateSeparatorForSystemMessages` | `boolean` | `false`  |
| `enableMessageGroupingByUser`         | `boolean` | `true`   |
| `enforceUniqueReaction`               | `boolean` | `false`  |
| `hideDateSeparators`                  | `boolean` | `false`  |
| `hideStickyDateHeader`                | `boolean` | `false`  |
| `maxTimeBetweenGroupedMessages`       | `number`  | no limit |

#### Keyboard, audio and capabilities

| Prop                                  | Type                                      | Default                          |
| ------------------------------------- | ----------------------------------------- | -------------------------------- |
| `additionalKeyboardAvoidingViewProps` | `Partial<KeyboardCompatibleViewProps>`    | none                             |
| `allowConcurrentAudioPlayback`        | `boolean`                                 | `false`                          |
| `disableKeyboardCompatibleView`       | `boolean`                                 | `false`                          |
| `keyboardBehavior`                    | `KeyboardCompatibleViewProps['behavior']` | `"padding"`                      |
| `keyboardVerticalOffset`              | `number`                                  | `86.5` on iOS, `-300` on Android |
| `overrideOwnCapabilities`             | `Partial<OwnCapabilitiesContextValue>`    | none                             |

## Related hooks

| Hook                    | Returns                                                                                |
| ----------------------- | -------------------------------------------------------------------------------------- |
| `useThreadContext()`    | `{ thread, allowThreadMessagesInChannel }`; `thread` is `undefined` outside a `Thread` |
| `useChannel()`          | inside a thread, the thread's channel                                                  |
| `useMessagePaginator()` | inside a thread, `thread.messagePaginator`                                             |
| `useMessageComposer()`  | inside a thread, `thread.messageComposer`                                              |
| `useTypingString()`     | who is typing in the thread                                                            |

See [ThreadContext](https://getstream.io/chat/docs/sdk/react-native/component-reference/thread-list/threadcontext/), [useThreadContext](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/state-and-entity-hooks/#usethreadcontext) and, for the thread's state and methods, [Thread and thread manager](https://getstream.io/chat/docs/sdk/react-native/reference/state/thread-and-thread-manager/).

---

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