# Thread and thread manager

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

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-native/v10/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-native/v10/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.

A thread from `client.threads.ensure()` loads its data the first time it's opened, unless its parent message has no replies and its channel is watched. Then there is nothing to load, and the first reply arrives as an event.

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

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

The SDK's components get threads this way. Tapping a message's reply count or its "Thread Reply" action calls `onThreadSelect` with `client.threads.ensure()` for that message, and the `Thread` component activates the thread it renders.

### 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-native/v10/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. The `ThreadList` component renders `paginator.state` and calls `paginator.toTail()` when the user scrolls to the end.

### 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, sized to the loaded threads plus the unseen ones, at most one page. 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. In this SDK, `Chat` calls `client.threads.registerSubscriptions()` when it mounts and `client.threads.unregisterSubscriptions()` when it unmounts. `ThreadList` calls `client.threads.activate()` while its `isFocused` prop is `true`, which is the default, and `client.threads.deactivate()` when it turns `false` or the list unmounts. Pass `isFocused` from your navigation, so that the list loads the threads it missed when its screen comes back into view.

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

`connection.recovered` isn't dispatched after a reconnect when `connectionRecovery.enabled` is `false` under the `client` key of `client.config`, so the list doesn't reload after a reconnect then.

## 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-native/v10/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-native/v10/reference/state/messagecomposer-api/).                         |
| `thread.messageOperations` | Sends, retries, edits and deletes replies. See [Message operations](https://getstream.io/chat/docs/sdk/react-native/v10/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.                  |

The replies aren't in `thread.state`. They're in `thread.messagePaginator.state`, under `items`.

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

Three getters return the value at the moment you read them, and don't tell you when it changes:

- `thread.hasStaleState` reads `isStateStale`.
- `thread.active` reads `active`.
- `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 `Thread` component does this for the thread it renders. It activates the thread until it unmounts and reloads it while its state is stale. It loads the first page of replies with `MessagePaginator.toTail()` when none are loaded, and it watches the thread's channel when that channel was never loaded, which happens for a thread opened from the thread list. See [`Thread`](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/thread-list/thread/).

With [offline support](https://getstream.io/chat/docs/sdk/react-native/v10/state/offline-support/offline-support/), the local database doesn't keep replies that are only in their thread, so a thread needs a connection to load its replies.

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-native/v10/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-native/v10/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-native/v10/state/channels-and-threads/read-state-and-unread-counts/) explains thread unread counts.
- [Message paginator](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/message-paginator/) lists the API of a thread's replies.
- [Replies and threads](https://getstream.io/chat/docs/sdk/react-native/v10/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-native/v10/reference/state/thread-and-thread-manager/](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/thread-and-thread-manager/).