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

Thread and thread manager

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. The thread list as a feature is on 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.

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 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.
get(id) Thread or undefined See 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:

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 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.
thread.messageComposer The composer for replies. See MessageComposer API.
thread.messageOperations Sends, retries, edits and deletes replies. See 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:

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:

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.

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