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

Thread UI

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 and 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:

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 explains the thread's state.

With ChatView, ThreadSlot renders Thread for the thread in its slot, as in 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 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 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 and Message composer UI list them.

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

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