Shared entities
The client keeps one object for each channel and each thread, and one copy of each message. Every channel list, every search result and your own code get the same objects, so a change made in one place shows everywhere.
This page covers how to get those objects and how to load them. How long the client keeps them is on Entity lifecycle.
One instance per id
One Channel and one Thread per id
Example
Say the channel "general" is in the user's channel list and is also open on screen. Both use the same Channel object. When a message arrives in "general", the list row and the open channel both show it, because they read the same stores.
The client finds them by id:
// undefined when the client has no channel with this cid
const general = client.channelManager.get("messaging:general");
// A thread's id is the id of its parent message.
const thread = client.threads.get(messageId);Messages are shared, read-only snapshots
The client keeps one copy of each message. A message shown in the channel's message list and as the parent of a thread is that one copy. When the user adds a reaction to it, the reaction shows in the channel's list and in the thread at once.
The message objects you get are snapshots of that copy. Each update replaces the message with a new object, so code that compares objects sees the change. Changing a field on a message object you received doesn't update the message, and no list shows the change. The next update of that message discards it.
Change messages through the SDK's methods instead, such as channel.messageOperations.update() for an edit and channel.messageOperations.addReaction() for a reaction. See Message operations.
Getting an instance
client.channel
client.channel(type, id, custom) returns the channel for a type and an id: the one the client has, or a new one, which the client keeps from then on. It sends no request.
const general = client.channel("messaging", "general");
const same = client.channel("messaging", "general");
console.log(general === same); // trueA new channel holds only the data you passed until it's loaded, as described in Loading. For a channel the client already has, client.channel() applies only custom and ignores the rest.
It needs a user: call it after client.connectUser(), which doesn't have to finish first. Without a user, it throws.
Provisional channels
A channel you create from its members, without an id, is provisional until the server answers its first query. That's the usual way to start a direct message: you know who's in it, and the server either creates the channel or returns the one these members already have. If the client already has that conversation, you get the loaded channel straight away.
import type { StreamChat } from "stream-chat";
// The direct message between the current user and another user, created if it doesn't exist yet.
export const getDirectMessage = (client: StreamChat, otherUserId: string) =>
client
.channel("messaging", {
members: [{ user_id: client.userId! }, { user_id: otherUserId }],
})
.watch();The SDK's components and ChatView handle the rest. If your own code stores the channel or its cid, for example in a URL or a map, wait for Channel.watch() and store the channel it returns. Its cid is final from then on; before that, it's temporary.
A channel you create with an id is never provisional.
Threads
Each parent message has one thread object. client.threads.ensure() returns it: the one the client already has, for example from the thread list, or a new one. Like client.channel(), it sends no request.
const thread = client.threads.ensure({ channel, parentMessage: message });Get threads this way, not with new Thread(). A thread from ThreadManager.ensure() is the one the thread list and the layout use, and it receives events. Opening a thread in the UI calls ThreadManager.ensure() for you. You call it yourself when your code needs the thread object: to render a thread in a layout of your own, to read a thread's state in a component of your own, or to load its replies ahead of time.
A new thread holds only its parent message. Its replies load when the UI shows it. Without a UI, call Thread.activate(): it loads them, and returns the function that releases the thread when you're done.
What not to do
- Don't create instances with
new Channel()ornew Thread(). The client doesn't keep an object you construct: events don't update it, and channel paginators hold the client's own instance instead. Useclient.channel()orThreadManager.ensure(). - Don't keep the threads that
client.getThreadAndHydrate()andclient.queryThreadsAndHydrate()return. They return the client's instance when it has one for that thread. Otherwise they return a separate object, which the client doesn't keep and events don't update. Get threads throughclient.threads.ensure()orclient.threads.get().
Loading
watch and query
There are two ways to load a channel:
Channel.watch()makes a channel ready to show. It loads the channel (its data, members, read state and latest messages) and subscribes this connection to its events. Calling it again is safe, even from several places at once: it sends a request only when the channel isn't loaded yet, or when you pass options it wasn't watched with.Channel.query(options)loads the channel with the options you pass, and always sends a request. It watches the channel only when they includewatch: true.
So call Channel.watch() wherever a channel is about to be shown, as often as needed, even from several places at once: a click, a notification, a deep link. It resolves with the channel to use from then on:
const openChannel = async (id: string) => {
const opened = await client.channel("messaging", id).watch();
// `opened` is watched. Use this object from here on.
return opened;
};To open a channel at a particular message, for example from a deep link, watch it and then jump to the message:
import type { Channel } from "stream-chat";
const openAtMessage = async (channel: Channel, messageId: string) => {
const opened = await channel.watch();
await opened.messagePaginator.jumpToMessage(messageId);
return opened;
};Don't pass messages options to Channel.watch() for this: options the current watch wasn't sent with make it send another watch request. MessagePaginator.jumpToMessage() loads the page around the message only when it isn't loaded; see Message paginators.
reload
Channel.reload() watches the channel again and refreshes its newest loaded messages, which repeating Channel.watch() doesn't do. It refreshes as many as it had loaded, not only the latest page. It catches up on what the client missed: new messages, edits, and messages deleted on the server in the meantime. Messages that failed to send stay in the list.
It does nothing for a channel that was never loaded. After a reconnect, the client reloads active channels for you; see Keeping a channel in memory and Connection.
Loading flags
Three flags in channel.state say how far a channel is loaded:
| Flag | What it says |
|---|---|
initialized |
A live server response was applied to this object: a query, a watch or a channel list page. It never goes back to false. While it's false, nothing has confirmed that the channel exists on the server. |
offlineMode |
The channel's data was restored from a local offline database, not loaded from the server. |
watchStatus |
"watching": events arrive. "wasWatching": the connection dropped, so the watch ended and should be restored. "notWatching": never watched, or watching was stopped on purpose. The values are in ChannelWatchStatus. |
They're in the store, so subscribe to them:
import { ChannelWatchStatus } from "stream-chat";
const unsubscribe = channel.state.subscribeWithSelector(
({ initialized, watchStatus }) => ({ initialized, watchStatus }),
({ initialized, watchStatus }) => {
if (!initialized) console.log("Not loaded yet");
else if (watchStatus === ChannelWatchStatus.Watching) console.log("Live");
else console.log("Loaded, not live");
},
);channel.initialized, channel.offlineMode and channel.watchStatus read the same values once, without subscribing.
Watching needs a connection
The server ties a watch to this client's WebSocket connection. So:
Channel.watch()and any query withwatch: truewait until the WebSocket has connected. Calling them right afterclient.connectUser(), without waiting for it, is fine.- With no WebSocket open or opening, for example after
client.closeConnection(), they fail at once. - To stop waiting, pass a signal:
Channel.watch(undefined, { signal }). Channel.query()withoutwatch: truedoesn't need the WebSocket. It loads the channel without watching it.
Watching keeps the channel current only while the connection lasts. What the client does after a reconnect is on Connection.