# Thread and thread manager

> **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 the client's object for the replies to one message. `client.threads`, a `ThreadManager`, keeps the client's threads, the user's thread list and the unread thread count. This page lists the API of both in `stream-chat`.

What a thread holds is explained on [What channels and threads hold](https://getstream.io/chat/docs/sdk/react/v15/state/channels-and-threads/what-channels-and-threads-hold/#thread). The thread list as a feature is on [Replies and threads](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/replies-and-threads/).

## client.threads

The client creates `client.threads` when it's constructed. It keeps one `Thread` per parent message id, whether a thread is in the thread list or was opened from a message.

### Getting a thread

| Call                                                  | Returns                                                                                                                                                  |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client.threads.ensure({ channel, parentMessage })`   | The thread of `parentMessage`, or a new one that `client.threads` keeps from then on. Sends no request.                                                  |
| `client.threads.get(id)`                              | The thread with that id, from the list or opened this session, or `undefined`. A thread's id is its parent message's id.                                 |
| `client.getThreadAndHydrate({ message_id }, options)` | Loads one thread from the server. Resolves with the kept instance when there is one, and otherwise with a new object that `client.threads` doesn't keep. |
| `client.queryThreadsAndHydrate(options)`              | Loads a page of the user's threads, with the same rule for each thread. The thread list uses it.                                                         |

Don't construct `new Thread()`, and don't keep the objects the last two return when `client.threads.get()` doesn't know them: events don't update them.

```ts
const thread = client.threads.ensure({ channel, parentMessage: message });

// The user opened the thread. Call release() when they close it.
const release = thread.activate();
```

### State

`client.threads.state` holds the user's threads as a whole:

| Field                     | Type       | What it holds                                                                                             |
| ------------------------- | ---------- | --------------------------------------------------------------------------------------------------------- |
| `unreadThreadCount`       | `number`   | How many of the user's threads have unread replies. The server sends it, so it counts threads not loaded. |
| `unseenThreadIds`         | `string[]` | Threads that got a reply since the thread list last loaded, and aren't in it.                             |
| `isThreadOrderStale`      | `boolean`  | A listed thread got a reply since the list last loaded, so its order may be out of date.                  |
| `active`                  | `boolean`  | The thread list is on screen, set by `client.threads.activate()` and `client.threads.deactivate()`.       |
| `wasActivatedAtLeastOnce` | `boolean`  | `client.threads.activate()` was called this session. Only then does a reconnect reload the list.          |

### The thread list

`client.threads.paginator` is a `ThreadPaginator`, a list like the others: its `state` store has `items`, `isLoading`, `hasMoreTail` and `lastQueryError`. [Channel manager and ChannelPaginator](https://getstream.io/chat/docs/sdk/react/v15/reference/state/channel-manager-and-channelpaginator/#the-base-paginator) lists what every list has. The thread list differs from the others:

- It loads 25 threads a page, with 10 participants and 10 replies each, and watches their channels.
- It keeps the server's order. There is no client-side sort, so a reply doesn't move a thread: `isThreadOrderStale` becomes `true` instead.
- `hasMoreTail` is `false` until the first page has loaded, so `ThreadPaginator.toTail()` does nothing before that.
- `ThreadPaginator.reload({ limit })` loads the first page again and replaces the list once the response arrives. The threads stay on screen meanwhile. Only the first load sets `isLoading`.

Load it through `client.threads.reload()`, below, rather than through the paginator.

### Methods

| Method                               | Returns                 | What it does                                                                                                                                                                                                                  |
| ------------------------------------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ensure({ channel, parentMessage })` | `Thread`                | See [Getting a thread](#getting-a-thread).                                                                                                                                                                                    |
| `get(id)`                            | `Thread` or `undefined` | See [Getting a thread](#getting-a-thread).                                                                                                                                                                                    |
| `activate()`                         | `void`                  | Sets `active`. While subscriptions are registered, it loads the list if needed.                                                                                                                                               |
| `deactivate()`                       | `void`                  | Clears `active`.                                                                                                                                                                                                              |
| `reload({ force })`                  | `Promise<void>`         | Loads the list when it hasn't loaded, has unseen threads or a stale order. A successful load clears `unseenThreadIds` and `isThreadOrderStale`. `force: true` loads it in any case. A call made during a reload does nothing. |
| `registerSubscriptions()`            | `void`                  | Starts handling events for the list and every kept thread. Calling it again does nothing.                                                                                                                                     |
| `unregisterSubscriptions()`          | `symbol`                | Stops handling events.                                                                                                                                                                                                        |
| `resetState()`                       | `void`                  | Releases every thread, empties the list and resets the state.                                                                                                                                                                 |
| `config`, `configState`              | `ThreadManagerConfig`   | The resolved configuration, and the same value as a store.                                                                                                                                                                    |
| `updateConfig(patch)`                | `void`                  | Changes the configuration.                                                                                                                                                                                                    |

`client.threads.activate()` and `client.threads.deactivate()` are a plain on and off flag, not counted. `client.threads.registerSubscriptions()` isn't counted either: one `client.threads.unregisterSubscriptions()` stops it. Code that uses `stream-chat` alone calls `client.threads.registerSubscriptions()` itself; the React SDK calls it for you.

The configuration has one setting, `connectionRecoveryThrottleMs`: the minimum time between reloads after reconnects, 1000 ms by default. It's read when the subscriptions are registered, so a change applies from the next `client.threads.registerSubscriptions()`. Register it under the `client` key:

```ts
client.config.set({
  client: { threads: { connectionRecoveryThrottleMs: 2000 } },
});
```

### Events

While its subscriptions are registered, `client.threads` handles these events:

| Event                                                                                                                                                                                         | What changes                                                                                           |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `health.check`, `connection.ok`, `notification.mark_read`, `notification.mark_unread`, `notification.thread_message_new`, `notification.channel_deleted`                                      | `unreadThreadCount`, when the event carries it.                                                        |
| `notification.thread_message_new`                                                                                                                                                             | `isThreadOrderStale` for a listed thread, `unseenThreadIds` for another one, once the list has loaded. |
| `connection.recovered`                                                                                                                                                                        | Reloads the list with `force: true`, if it was activated this session.                                 |
| `message.new`, `message.read`, `notification.mark_unread`, `thread.updated`, `message.updated`, `message.undeleted`, `message.deleted`, `reaction.*`, `user.messages.deleted`, `user.deleted` | Hands each event to the threads it concerns. See [Thread events](#thread-events).                      |

## Thread state

A thread's identity and its objects:

| Property                   | What it is                                                                                                                                  |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `thread.id`                | The parent message's id.                                                                                                                    |
| `thread.channel`           | The channel the thread belongs to.                                                                                                          |
| `thread.state`             | The store described below.                                                                                                                  |
| `thread.messagePaginator`  | The replies. See [Message paginator](https://getstream.io/chat/docs/sdk/react/v15/reference/state/message-paginator/#thread-wiring-and-minimal-initialization). |
| `thread.messageComposer`   | The composer for replies. See [MessageComposer API](https://getstream.io/chat/docs/sdk/react/v15/reference/state/messagecomposer-api/).                         |
| `thread.messageOperations` | Sends, retries, edits and deletes replies. See [Message operations](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/message-operations/).   |

`thread.state` holds these fields. The timestamps are in nanoseconds:

| Field           | Type                                 | What it holds                                                                                           |
| --------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `parentMessage` | `LocalMessage`                       | The message the thread replies to. The same message the channel's list shows, so an edit shows in both. |
| `replyCount`    | `number`                             | How many replies the thread has, from the parent message's `reply_count`.                               |
| `participants`  | `ThreadParticipant[]` or `undefined` | The users who replied.                                                                                  |
| `read`          | `ThreadReadState`                    | Each participant's read state in the thread, by user id. The fields are in the next table.              |
| `channel`       | `Channel`                            | The channel the thread belongs to.                                                                      |
| `title`         | `string`                             | The thread's title.                                                                                     |
| `custom`        | `CustomThreadData`                   | The thread's custom data.                                                                               |
| `createdAt`     | `TimestampNS`                        | When the thread was created.                                                                            |
| `updatedAt`     | `TimestampNS` or `null`              | When the thread was last updated.                                                                       |
| `deletedAt`     | `TimestampNS` or `null`              | When the thread or its parent message was deleted.                                                      |
| `active`        | `boolean`                            | Something has activated the thread with `Thread.activate()` and not released it yet.                    |
| `isLoading`     | `boolean`                            | `Thread.reload()` is running.                                                                           |
| `isStateStale`  | `boolean`                            | The thread needs loading again: it knows only its parent message, or it missed events.                  |

A thread's read state uses camelCase, unlike a channel's:

| Field                  | What it holds                                               |
| ---------------------- | ----------------------------------------------------------- |
| `user`                 | The user.                                                   |
| `lastReadAt`           | When the user last read the thread.                         |
| `unreadMessageCount`   | How many replies the user hasn't read.                      |
| `lastReadMessageId`    | The last reply the user read, when known.                   |
| `firstUnreadMessageId` | The reply the user marked unread from, after a mark unread. |

Two getters read the state once, without subscribing:

- `thread.hasStaleState` reads `isStateStale`.
- `thread.ownUnreadCount` reads the current user's `unreadMessageCount`, or `0`.

This handler runs whenever the reply count or the current user's unread count changes:

```ts
const ownUserId = client.userId;

const unsubscribe = thread.state.subscribeWithSelector(
  ({ read, replyCount }) => ({
    replyCount,
    unread: ownUserId ? (read[ownUserId]?.unreadMessageCount ?? 0) : 0,
  }),
  ({ replyCount, unread }) => {
    console.log(`${replyCount} replies, ${unread} unread`);
  },
);
```

### Thread events

A thread doesn't listen to the client itself. `client.threads` hands it the events about it, as long as the thread is kept and the manager's subscriptions are registered:

| Event                                                     | What changes                                                                                                                                  |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `message.new` with the thread's `parent_id`               | The reply joins the list. Each `unreadMessageCount` in `read` grows, except the sender's and, while the thread is active, the current user's. |
| `message.read` for the thread                             | The reader's entry in `read`.                                                                                                                 |
| `notification.mark_unread` for the thread                 | The reader's entry in `read`.                                                                                                                 |
| `thread.updated`                                          | `title`, `custom`, `updatedAt` and `deletedAt`.                                                                                               |
| `message.updated`, `message.undeleted`, `message.deleted` | The reply in the list, or the parent message.                                                                                                 |
| `reaction.new`, `reaction.updated`, `reaction.deleted`    | Replies that quote the message.                                                                                                               |
| `user.messages.deleted`, `user.deleted`                   | That user's replies, in every kept thread.                                                                                                    |

A change to the parent message from anywhere, such as a reaction, also updates `parentMessage`, `replyCount`, `participants` and `deletedAt`.

## Thread methods

| Method                      | Returns                              | What it does                                                                                                                                                                                                                   |
| --------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `activate()`                | `() => void`                         | Marks the thread active and returns the function that releases it. Activations are counted. The first one also keeps the thread in `client.threads` for the session.                                                           |
| `markRead({ force })`       | A promise of the response, or `null` | Marks the thread read through `client.messageDeliveryReporter`, with `requestHandlers.markReadRequest` when registered. Resolves with `null` without a request while the user has no unread replies, unless `force` is `true`. |
| `reload()`                  | `Promise<void>`                      | Loads the thread again and refreshes its loaded replies, keeping replies that failed to send. Does nothing while a reload runs.                                                                                                |
| `registerSubscriptions()`   | `void`                               | Starts the thread's own subscriptions. `client.threads` calls it for every thread it keeps.                                                                                                                                    |
| `unregisterSubscriptions()` | `symbol`                             | Stops them and marks the thread stale.                                                                                                                                                                                         |
| `config`, `configState`     | `ThreadConfig`                       | The resolved configuration, and the same value as a store.                                                                                                                                                                     |
| `updateConfig(patch)`       | `void`                               | Changes the configuration.                                                                                                                                                                                                     |

While a thread is active and its subscriptions are registered:

- a reply that would be unread is marked read instead, through `Thread.markRead()`, throttled;
- when its state is stale, it reloads.

`Thread.reload()` rejects when the request fails, except for a thread that has no replies yet: the server doesn't know it, so that answer is ignored. A connection that stays up while the channel stops being watched marks the thread stale. After a reconnect, the client reloads active threads and marks the others stale.

The configuration has one setting, `requestHandlers.markReadRequest`. It replaces the request `Thread.markRead()` sends, and is registered under the `thread` key:

```ts
client.config.set({
  thread: {
    requestHandlers: {
      markReadRequest: async ({ options, thread }) => {
        console.log("Marking read", thread.id);
        return await thread.channel.markRead(options);
      },
    },
  },
});
```

The `thread` key also holds `messagePaginator` and `messageOperations`, which go to the thread's reply list and message operations. See [Configuration tree](https://getstream.io/chat/docs/sdk/react/v15/reference/state/configuration-tree/).

Threads also have public methods that `client.threads` calls to apply events: `handleNewReply`, `handleRepliesRead`, `handleRepliesUnread`, `handleThreadUpdated`, `handleMessageUpdated`, `handleMessageDeleted`, `handleReactionChanged`, `handleUserMessagesDeleted`, `upsertReplyLocally`, `deleteReplyLocally`, `updateParentMessageLocally`, `updateParentMessageOrReplyLocally` and `hydrateState`. App code doesn't call them. `Thread.markAsRead()` is a deprecated name for `Thread.markRead()`.

## See also

- [What channels and threads hold](https://getstream.io/chat/docs/sdk/react/v15/state/channels-and-threads/what-channels-and-threads-hold/) explains a thread's state.
- [Read state and unread counts](https://getstream.io/chat/docs/sdk/react/v15/state/channels-and-threads/read-state-and-unread-counts/) explains thread unread counts.
- [Message paginator](https://getstream.io/chat/docs/sdk/react/v15/reference/state/message-paginator/) lists the API of a thread's replies.
- [Replies and threads](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/replies-and-threads/) shows the thread list and threads in the UI.

---

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