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

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

Info:

Example

Say the channel "general" is in the user's channel list and is also open on the channel 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

Channels

client.channel(type, id, data) returns the channel for a type and an id. That's 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");

// The client keeps one object per channel, so this is the same object.
const same = client.channel("messaging", "general");

console.log(general === same); // true

A 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 data.custom and ignores the rest of data.

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();

To show the conversation, you can also pass the provisional channel to Channel without watching it first. Channel watches it when it mounts, and renders the replacement if another object for the same conversation supersedes it (see Superseded channels). If your own code stores the channel or its cid, for example in navigation params 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. That's 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 UI use, and it receives events. Opening a thread from a message list calls ThreadManager.ensure() for you, and onThreadSelect receives its result. You call it yourself when your code needs the thread object: to open a thread from a push notification or a deep link, 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. Thread loads its replies when it mounts. Without the UI, call Thread.reload(), which loads them. Thread.activate() loads them too while Chat is mounted, because Chat registers the subscriptions of client.threads, which reload an active thread whose state is stale. It returns the function that releases the thread when you're done. See What channels and threads hold.

What not to do

  • Don't create instances with new Channel() or new 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. Use client.channel() and client.threads.ensure().
  • Don't keep the threads that client.getThreadAndHydrate() and client.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 through client.threads.ensure() or client.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 watched and loaded yet, or when you pass options the current watch wasn't made with, such as presence: true. A call made while the same watch is still waiting joins that request instead of sending another one. It resolves with the channel to use from then on.
  • Channel.query(options) loads the channel with the options you pass, and always sends a request. It watches the channel only when they include watch: true. It returns the messages it fetched but doesn't add them to the channel's message list. Load messages with Channel.watch() or with channel.messagePaginator.

Channel watches the channel for you when it mounts, unless the channel is already watched and loaded with its latest messages, or initializeOnMount is false. See Channel UI.

So call Channel.watch() wherever a channel is about to be shown, as often as needed, even from several places at once, such as a tap, a push notification and 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. The page it fetches is stored but not shown, and options the current watch wasn't made with make it send another watch request. MessagePaginator.jumpToMessage() loads the page around the message only when it isn't loaded, and shows it. 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, either from the server or from the offline database. 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 the local offline database, not loaded from the server. A later live response sets it back to false.
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.

With offline support, a channel list's first page shows the channels from the local database before the server answers. Those channels have offlineMode set and initialized still false. Channel also sets offlineMode when its watch fails, so the channel is reloaded after the next reconnect.

Chat closes the WebSocket while the app is in the background, unless its closeConnectionOnBackground prop is false. That turns every "watching" channel into "wasWatching". When the app returns to the foreground, the socket opens again and the client recovers as it does after any reconnect.

The flags are in the store, so subscribe to them:

import { ChannelWatchStatus } from "stream-chat";
import type { Channel } from "stream-chat";

// Returns the function that ends the subscription.
export const logLoadState = (channel: Channel) =>
  channel.state.subscribeWithSelector(
    ({ initialized, offlineMode, watchStatus }) => ({
      initialized,
      offlineMode,
      watchStatus,
    }),
    ({ initialized, offlineMode, watchStatus }) => {
      if (!initialized && !offlineMode) 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 with watch: true wait until the WebSocket has connected. Calling them right after client.connectUser(), without waiting for it, is fine.
  • With no WebSocket open or opening, for example after client.closeConnection(), they fail at once. This includes the time the app spends in the background, when Chat has closed the socket.
  • To stop waiting, pass a signal, as in Channel.watch(undefined, { signal }).
  • Channel.query() without watch: true doesn'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.