# Thread UI

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

A thread is a message and its replies, shown apart from the channel. Two parts of the UI show threads: `Thread`, which shows one thread, and `ThreadList`, which lists the user's threads. This page covers both, the thread header, and the parts you can replace. The props are in the [Thread](https://getstream.io/chat/docs/sdk/react/v15/component-reference/thread-list/thread/) and [ThreadList](https://getstream.io/chat/docs/sdk/react/v15/component-reference/thread-list/threadlist/) references.

## The Thread component

`<Thread thread={thread}>` shows the thread passed as `thread`. Like `Channel`, it renders no UI of its own, only its children:

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

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

Here is what `Thread` does:

- It gives the thread to its children. `useThreadContext()` returns it, and `useChannel()` returns the thread's channel, so a `Thread` needs no `Channel` around it.
- The children then work on the thread's objects. `MessageList` renders the replies in `thread.messagePaginator`, and `MessageComposer` writes into `thread.messageComposer`.
- It loads the thread while its state is stale. A thread created from its parent message starts stale when it may have replies to load. A thread from the thread list arrives loaded.
- It renders nothing until the thread has its parent message, and nothing when replies are turned off for the channel (`replies.enabled` in its configuration).
- It renders the thread's container, `.str-chat__thread`, around the children.

The parent message isn't a child you place. `MessageList` renders it above the replies, because it has to scroll with them: a date separator, the message, and the reply count under it.

Get the thread from `client.threads`, not with `new Thread()`. `client.threads.ensure({ channel, parentMessage })` returns the thread for a message, creating it if needed, and `client.threads.get(id)` returns one the client already has. Either way the client keeps one instance per thread and sends it its events. [What channels and threads hold](https://getstream.io/chat/docs/sdk/react/v15/state/channels-and-threads/what-channels-and-threads-hold/) explains the thread's state.

With `ChatView`, `ThreadSlot` renders `Thread` for the thread in its slot, as in [Add threads](https://getstream.io/chat/docs/sdk/react/v15/getting-started/quick-start/#add-threads). Without a layout, the replies button on a message does nothing, so the app picks the thread and renders `Thread` itself.

## The thread header

`ThreadHeader` shows, from start to end:

1. **`StartContent`**, by default the back button, `WorkspaceNavigationBackButton`. It shows only when the thread is stacked over other content in its panel.
2. **The title**, "Thread", with a subtitle: the channel's name and the number of replies, such as "Design · 4 replies". The `overrideTitle` prop replaces the channel's name.
3. **`EndContent`**, by default the close button, `WorkspaceNavigationCloseButton`. It shows only when the thread's panel can be closed, and closes it with `useCloseThread()`.

Like the channel header's buttons, both come from the layout, and neither renders without one. A component passed as `StartContent` or `EndContent` replaces the button; render the button yourself to keep it. The [Thread header recipe](https://getstream.io/chat/docs/sdk/react/v15/recipes/headers/thread-header/) builds a header of its own.

## The thread list

`ThreadList` lists the threads the user takes part in, in the server's order: unread threads first, then by latest reply. It reads `client.threads`:

1. When it mounts, it reloads the list. While the page is visible, it activates `client.threads`, so the list follows new replies; it deactivates it when the page is hidden or the list unmounts.
2. It subscribes to `client.threads.paginator.state` and renders a `ThreadListItem` for each thread. It loads the next page when the user scrolls to the end.
3. Clicking a row opens the thread through `openThread(thread)` from `useWorkspaceNavigation()`. ⌘/ctrl-click asks the layout to open it beside the current one, where the layout has room.

Each row reads its thread's stores: `thread.state` for the parent message, the channel, the participants, the reply count and the user's unread count, and `thread.messagePaginator.aggregateState` for the latest reply. `ThreadListItem` renders `ThreadListItemUI`, the row's look. Anything inside a row can read its thread with `useThreadListItemContext()`.

The list header shows the "Threads" title. Like the channel list header, it renders the `HeaderEndContent` override at its end while a thread is open.

### The unseen threads banner

A thread that isn't in the loaded list can get a reply, for example an older thread someone answers. The thread manager records it in `client.threads.state` as unseen, and the list shows a banner above the rows: "2 unread threads". Clicking it reloads the list, which brings those threads in at their new position.

### Loading, empty and error states

| State                  | What the list renders                  | Override key                 |
| ---------------------- | -------------------------------------- | ---------------------------- |
| Loading the first page | Placeholder rows                       | None                         |
| Loading the next page  | A spinner after the last row           | `ThreadListLoadingIndicator` |
| No threads             | "Reply to a message to start a thread" | `ThreadListEmptyPlaceholder` |

A reload keeps the loaded threads on screen until the new ones arrive. When a query fails, the list keeps what it had loaded, or shows the empty placeholder if it had nothing. The error is recorded as `lastQueryError` in `client.threads.paginator.state`; the list shows no error and publishes no notification.

## Replacing parts

Register replacements with `WithComponents`. [Overriding components](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/overriding-components/) explains how overrides apply.

| Override key                                               | Replaces                                                                    |
| ---------------------------------------------------------- | --------------------------------------------------------------------------- |
| `ThreadHead`                                               | The parent message above the replies. Gets the parent message as `message`. |
| `ThreadStart`                                              | The reply count under the parent message                                    |
| `ThreadListItem`                                           | The whole row. Gets `thread`.                                               |
| `ThreadListItemUI`                                         | The row's look                                                              |
| `ThreadListUnseenThreadsBanner`                            | The unseen threads banner                                                   |
| `ThreadListLoadingIndicator`, `ThreadListEmptyPlaceholder` | The states above                                                            |
| `HeaderEndContent`                                         | The end of the list header, while a thread is open                          |

The defaults of `ThreadHead`, `ThreadListUnseenThreadsBanner`, `ThreadListEmptyPlaceholder` and `ThreadListLoadingIndicator` aren't exported, so a replacement for one of them can't render the default inside it.

`Thread` and `ThreadHeader` have no override key. Render other children in `Thread`, and change `ThreadHeader` through its props. Inside a thread, the message list and the composer take the same override keys as anywhere else; [Message list UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/message-list-ui/#replacing-parts) and [Message composer UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/message-composer-ui/#replacing-parts) list them.

This thread list keeps the default row and shows the thread's title above it, when the thread has one:

```tsx title="src/ThreadsSidebar.tsx"
import type { ThreadState } from "stream-chat";
import type { ThreadListItemUIProps } from "stream-chat-react";
import {
  ThreadList,
  ThreadListItemUI,
  useStateStore,
  useThreadListItemContext,
  WithComponents,
} from "stream-chat-react";

const titleSelector = ({ title }: ThreadState) => ({ title });

const RowWithTitle = (props: ThreadListItemUIProps) => {
  const thread = useThreadListItemContext();
  const { title } = useStateStore(thread?.state, titleSelector) ?? {};

  return (
    <div className="thread-row">
      {title && <span className="thread-row__title">{title}</span>}
      <ThreadListItemUI {...props} />
    </div>
  );
};

const overrides = { ThreadListItemUI: RowWithTitle };

export const ThreadsSidebar = () => (
  <WithComponents overrides={overrides}>
    <ThreadList />
  </WithComponents>
);
```

## Used in

- [Replies and threads](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/replies-and-threads/)
- [Thread header](https://getstream.io/chat/docs/sdk/react/v15/recipes/headers/thread-header/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/ui/components/thread-ui/](https://getstream.io/chat/docs/sdk/react/v15/ui/components/thread-ui/).