# Replies and threads

> **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/).

## What users get

Any message can start a thread: a conversation of replies under it, shown on its own screen. Users also get a thread list with every thread they take part in, and can send a reply to the channel as well.

- **Starting and opening a thread.** **Thread Reply** in a message's menu opens the message's thread. A message with replies shows a "3 Replies" button under it, with the avatars of the people who replied, and pressing it opens the thread.
- **The thread screen.** The parent message sits at the top, followed by a "3 Replies" divider, the replies and a composer. The typing indicator at the bottom of the list shows who is typing a reply.
- **Also sending to the channel.** The thread's composer has an "Also send to channel" checkbox. Such a reply shows "Also sent in channel" in the thread, and "Replied to a thread" in the channel.
- **The thread list.** A list of the user's threads. Each row shows the channel's name, the parent message, the avatars of the participants, the reply count, the time of the latest reply and the user's unread count. When threads that aren't in the list get new replies, a banner ("2 new threads") offers to reload it.

The SDK has no thread header: the screen that renders the thread has your app's navigation header.

## How it works across the layers

### State: a thread and its replies

A thread is identified by its parent message: `thread.id` is the parent message's id. A reply is a message with a `parent_id`. With `show_in_channel: true`, it's also shown in the channel's message list.

There is one `Thread` instance per thread, shared by every component that shows it. `client.threads.ensure({ channel, parentMessage })` returns the existing instance or creates one, and `client.threads.get(id)` looks one up. A `Thread` holds what a channel holds, for its replies:

- `thread.state`: the parent message, the reply count, the participants, read state, and the flags `isLoading` and `isStateStale`;
- `thread.messagePaginator`: the replies;
- `thread.messageComposer`: the reply being written, with `showReplyInChannel` for the "Also send to channel" choice;
- `thread.messageOperations`: sending, editing and deleting replies. Requests go through the thread's channel, so a thread uses its channel's request handlers.

A thread created from its parent message knows only that message. It starts stale when the parent has replies or its channel isn't watched, and `Thread.reload()` loads its replies, participants and read state. See [What channels and threads hold](https://getstream.io/chat/docs/sdk/react-native/v10/state/channels-and-threads/what-channels-and-threads-hold/) for what a thread holds and how unread threads are counted.

### UI: opening a thread

The SDK doesn't navigate. **Thread Reply** and the replies button call `onThreadSelect` on the `MessageList` (or `MessageFlashList`), with the thread from `client.threads.ensure({ channel, parentMessage: message })`. Your callback opens a screen that renders the thread:

- `Thread` binds the thread to the components inside it, and renders only its children. It reloads the thread while its state is stale, loads the first page of replies when none are loaded, and activates the thread while it's mounted. An active thread marks new replies read as they arrive.
- `MessageList` inside it renders `thread.messagePaginator`. Its footer, `ThreadFooterComponent`, shows the parent message and the reply count divider. The list is inverted, so the footer sits at the top.
- `MessageComposer` inside it drives `thread.messageComposer`, and shows the "Also send to channel" checkbox (`ShowThreadMessageInChannelButton`) while `allowThreadMessagesInChannel` on `Thread` is `true`, which is the default.

`Thread` doesn't need a `Channel` around it. When the thread's channel hasn't been watched, for example when the thread was opened from the thread list, `Thread` watches it once, so the user's capabilities in the channel are known. The navigation itself is on [Navigation](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/navigation/) and [Moving between channels, threads and messages](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/moving-between-channels-threads-and-messages/).

`SentToChannelHeader` renders "Also sent in channel" and "Replied to a thread". It adds a **View** link only when you pass `onAlsoSentToChannelHeaderPress` to `Channel` and `Thread`. The handler receives the id of the message to show, and in a channel also the reply's thread, so you can navigate to the other copy.

### The thread list

The thread list is `client.threads`, the thread manager:

- `client.threads.paginator` holds the user's threads. Each page loads 25 threads, with up to 10 participants and the latest 10 replies of each.
- `client.threads.state` holds `unreadThreadCount`, `unseenThreadIds` and `isThreadOrderStale`.

`ThreadList` renders the paginator's threads, one `ThreadListItem` per thread, and loads the next page when the user scrolls to the end. Pressing a row calls its `onThreadSelect` prop with the thread. While its `isFocused` prop is `true`, which is the default, `ThreadList` keeps the thread manager active, and it deactivates the manager when it unmounts. Each activation reloads the list when it's empty, stale or has unseen threads. Pass the screen's focus state as `isFocused`, so a list on a tab that isn't shown stops reloading.

`Chat` starts the thread manager's event handling, so `unreadThreadCount` stays current even while no thread list is on screen.

#### Order

The list is in the order the server returns. The client doesn't sort it. A thread that gets a new reply updates its row in place, without moving.

#### New threads

When a thread gets a reply (`notification.thread_message_new`), the thread manager doesn't insert or move anything:

- If the thread is in the list, it sets `isThreadOrderStale`, because the server might now order the list differently.
- If it isn't, it adds the thread's id to `unseenThreadIds`. `ThreadListUnreadBanner` shows "N new threads" while that list isn't empty, and pressing it calls `client.threads.reload()`.

Both start only once the first page has loaded.

#### Reloading the list

`client.threads.reload()` asks the server for the first page again, sized to the threads already loaded plus the unseen ones (up to one page), and replaces the list in server order. Then it clears `unseenThreadIds` and `isThreadOrderStale`.

- Without `force`, it reloads only when the list is empty, stale or has unseen threads. The banner and each activation call it this way.
- `client.threads.reload({ force: true })` always reloads.
- After a reconnect, the thread manager reloads the list by itself, if it was ever activated. It does this on `connection.recovered`, which isn't dispatched after a reconnect when connection recovery is turned off.

The current threads stay on screen until the new page arrives. If the request fails, the list stays as it was, `client.threads.paginator.lastQueryError` holds the error, and the banner offers to retry while there are unseen threads to load.

## Turning it on or off

Replies are on by default. Two settings must both allow them:

- **On the client:** `channel.replies.enabled` in `client.config`.
- **On the server:** the channel type's `replies` setting, in the dashboard. When the server turns replies off, the client can't turn them on.

```ts
client.config.set({ channel: { replies: { enabled: false } } });
```

`channel.config.replies.enabled` holds the value in effect. The SDK's components don't read it: they follow the `send-reply` capability, which the server derives from the user's role and the channel type's settings. Without it, the message menu leaves out **Thread Reply**. The replies button still shows on messages that have replies.

To hide **Thread Reply** in your UI, pass `overrideOwnCapabilities={{ sendReply: false }}` to `Channel`, and don't render the thread screen.

## Customizing

### Composing the thread screen

`Thread` renders its children, so the screen is whatever you put inside: your own header, a `MessageFlashList` instead of `MessageList`, or extra content. `Thread` takes the same configuration props as `Channel`, and a `Channel` around it passes none down, so give both the same ones:

```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,
  onOpenThread,
}: {
  channel: StreamChannel;
  onOpenThread: (thread: StreamThread) => void;
}) => (
  <Channel channel={channel} enforceUniqueReaction>
    <MessageList onThreadSelect={onOpenThread} />
    <MessageComposer />
  </Channel>
);

export const ThreadScreen = ({ thread }: { thread: StreamThread }) => (
  <Thread enforceUniqueReaction thread={thread}>
    <MessageList />
    <MessageComposer />
  </Thread>
);
```

`onOpenThread` navigates to the screen that renders `ThreadScreen`. See [Thread UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/thread-ui/).

### Replacing the parent message area

The parent message and the divider are the thread list's footer. Pass `FooterComponent` to the `MessageList` inside `Thread` to replace them. This one shows the parent's text and how many people replied:

```tsx
import { Text, View } from "react-native";
import type { Thread as StreamThread, ThreadState } from "stream-chat";
import {
  MessageComposer,
  MessageList,
  Thread,
  useStateStore,
  useThreadContext,
} from "stream-chat-react-native";

const parentSelector = (state: ThreadState) => ({
  parentMessage: state.parentMessage,
  participants: state.participants,
  replyCount: state.replyCount,
});

const ThreadStartSummary = () => {
  const { thread } = useThreadContext();
  const { parentMessage, participants, replyCount } =
    useStateStore(thread?.state, parentSelector) ?? {};

  if (!parentMessage) return null;

  return (
    <View>
      <Text>{parentMessage.text}</Text>
      <Text>
        {replyCount ?? 0} replies from {participants?.length ?? 0} people
      </Text>
    </View>
  );
};

export const ThreadScreen = ({ thread }: { thread: StreamThread }) => (
  <Thread thread={thread}>
    <MessageList FooterComponent={ThreadStartSummary} />
    <MessageComposer />
  </Thread>
);
```

The default footer also shows the loading indicator for older replies. To keep the parent message and add to it, render `ThreadFooterComponent` inside your footer. It takes `parentMessagePreventPress`, `true` by default, which keeps presses on the parent message from doing anything.

### Replacing a thread list row

`ThreadListItem` is an override key. A replacement reads its row from `useThreadListItemContext()`, and opens the thread with `onThreadSelect` from `useThreadsContext()`:

```tsx
import { Pressable, Text } from "react-native";
import {
  useThreadListItemContext,
  useThreadsContext,
  WithComponents,
} from "stream-chat-react-native";

const CompactThreadRow = () => {
  const { lastReply, ownUnreadMessageCount, parentMessage, thread } =
    useThreadListItemContext();
  const { onThreadSelect } = useThreadsContext();

  return (
    <Pressable onPress={() => onThreadSelect?.(thread)}>
      <Text numberOfLines={1}>{parentMessage?.text}</Text>
      <Text numberOfLines={1}>{lastReply?.text}</Text>
      {ownUnreadMessageCount > 0 ? (
        <Text>{ownUnreadMessageCount} new</Text>
      ) : null}
    </Pressable>
  );
};

export const App = () => (
  <WithComponents overrides={{ ThreadListItem: CompactThreadRow }}>
    {/* the rest of the app */}
  </WithComponents>
);
```

The context also holds the thread's `channel`, the `draftMessage` when the thread's composer has unsent content, and the formatted `dateString`. `ThreadListComponent`, `ThreadListItemMessagePreview`, `ThreadListUnreadBanner`, `ThreadListEmptyPlaceholder`, `ThreadListLoadingIndicator` and `ThreadListLoadingMoreIndicator` are override keys too. The [Custom thread list](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/app/custom-thread-list/) UI cookbook builds a whole list.

### Showing the unread thread count

`client.threads.state.unreadThreadCount` holds the number of threads with unread replies, for a badge on a tab:

```tsx
import { Text } from "react-native";
import type { ThreadManagerState } from "stream-chat";
import { useChatContext, useStateStore } from "stream-chat-react-native";

const unreadThreadsSelector = (state: ThreadManagerState) => ({
  unreadThreadCount: state.unreadThreadCount,
});

export const UnreadThreadsBadge = () => {
  const { client } = useChatContext();
  const { unreadThreadCount } = useStateStore(
    client.threads.state,
    unreadThreadsSelector,
  );

  if (!unreadThreadCount) return null;

  return <Text>{unreadThreadCount}</Text>;
};
```

## Reference

- State: [What channels and threads hold](https://getstream.io/chat/docs/sdk/react-native/v10/state/channels-and-threads/what-channels-and-threads-hold/), [Message paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/) and [Thread and thread manager](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/thread-and-thread-manager/).
- UI: [Thread UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/thread-ui/), [Navigation](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/navigation/) and [Moving between channels, threads and messages](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/moving-between-channels-threads-and-messages/).
- Props: [Thread](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/thread-list/thread/), [ThreadList](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/thread-list/threadlist/), [ThreadListItem](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/thread-list/threadlistitem/), [ThreadContext](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/thread-list/threadcontext/) and [MessageList](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message-list/messagelist/).
- Hooks: [Channel and thread hooks](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/channel-and-thread-hooks/).

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/replies-and-threads/](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/replies-and-threads/).