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

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 explains what it does, and 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:

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 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;
  • 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:

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

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

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.

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
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, useThreadContext and, for the thread's state and methods, Thread and thread manager.