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

How components connect to state

Components in the UI layer don't fetch or keep chat data. They read it from objects in the state layer, and three components hand those objects down the tree: Chat gives the client, Channel gives a channel, and Thread gives a thread. Every component inside reads from the nearest one.

Take this tree:

import type {
  Channel as StreamChannel,
  Thread as StreamThread,
} from "stream-chat";
import {
  Channel,
  ChannelHeader,
  Chat,
  MessageComposer,
  MessageList,
  Thread,
  ThreadHeader,
} from "stream-chat-react";

type ConversationProps = {
  channel: StreamChannel;
  thread?: StreamThread;
};

export const Conversation = ({ channel, thread }: ConversationProps) => (
  <Chat client={client}>
    <Channel channel={channel}>
      <ChannelHeader />
      <MessageList />
      <MessageComposer />
    </Channel>
    {thread && (
      <Thread thread={thread}>
        <ThreadHeader />
        <MessageList />
        <MessageComposer />
      </Thread>
    )}
  </Chat>
);

The two MessageLists are the same component. The first renders channel.messagePaginator, because the nearest owner is the channel. The second renders thread.messagePaginator, because the nearest owner is the thread. The two MessageComposers work the same way: the first writes into channel.messageComposer, the second into thread.messageComposer. No prop says which one to use.

With a layout, you don't render Channel and Thread yourself: ChannelSlot and ThreadSlot render them for the channel or thread in their slot. See Workspace with ChatView.

Chat

Chat goes around every other SDK component. It takes a connected client, such as the one useCreateChatClient returns (see Quick start).

It provides:

  • the client, the client's channel manager, a search controller and the users the current user has muted, through ChatContext;
  • translations, through TranslationContext, from the i18nInstance you pass or a default English one;
  • the dialog manager for modals, and the screen-reader announcer;
  • the display filter that decides where notifications appear (see Displaying notifications).

While it's mounted, it also:

  • starts the event subscriptions of client.channelManager, client.threads, client.polls and client.reminders, and the reminders' timers, so channel lists, the thread list, polls and reminders follow events. It stops them when it unmounts.
  • publishes a notification while the connection is lost. It renders no banner for it: see System notifications.

Chat doesn't connect the user, load channels or decide where anything opens.

The search controller

Chat creates a SearchController with a channel, a user and a message source, and the search components read it from ChatContext. It disposes of that controller when it unmounts.

To use other sources, create the controller yourself and pass it as searchController. A controller you pass is yours to manage, so register and dispose of it the way Chat does:

import { useEffect, useMemo } from "react";
import {
  ChannelSearchSource,
  SearchController,
  UserSearchSource,
} from "stream-chat";
import type { StreamChat } from "stream-chat";
import { Chat } from "stream-chat-react";
import type { PropsWithChildren } from "react";

export const ChatWithSearch = ({
  children,
  client,
}: PropsWithChildren<{ client: StreamChat }>) => {
  // Channels and users only: no message search.
  const searchController = useMemo(
    () =>
      new SearchController({
        client,
        sources: [
          new ChannelSearchSource(client),
          new UserSearchSource(client),
        ],
      }),
    [client],
  );

  useEffect(() => {
    searchController.registerSubscriptions();
    return () => searchController.dispose();
  }, [searchController]);

  return (
    <Chat client={client} searchController={searchController}>
      {children}
    </Chat>
  );
};

The sources and their state are explained on Search sources.

Channel and Thread

Channel binds one channel to the components inside it. It takes a channel and nothing else:

  • It doesn't load the channel. Watch the channel before you pass it, for example with Channel.watch(), as Show one channel does. A channel from a channel list is already loaded, and the SDK's search results watch a channel before they open it.
  • It activates the channel while it's mounted. An active channel stays loaded and is reloaded after a reconnect. See Entity lifecycle.
  • It renders the channel's container, the str-chat__channel element, around its children. To fill the same place while no channel is selected, render ChannelPlaceholder.

Thread binds one thread. It takes a thread and renders no UI of its own, only its container, the str-chat__thread element, around its children:

  • Get the thread from the client, with client.threads.ensure({ channel, parentMessage }), or from the thread list. Never construct one: a thread the client doesn't know receives no events.
  • It loads the thread while its state is stale. A thread created from its parent message starts stale when it may have replies to load.
  • It renders nothing until the parent message is known, and nothing when replies are turned off for the channel's type.
  • It doesn't need a Channel around it. Inside a Thread, useChannel() returns the thread's channel.

Which object each part of a channel or thread holds is on What channels and threads hold.

Contexts

Your own components read the same objects through context hooks. This one sits inside a Channel or a Thread:

import {
  useChannel,
  useChatContext,
  useThreadContext,
} from "stream-chat-react";

export const WhereAmI = () => {
  const { client } = useChatContext();
  const channel = useChannel();
  const thread = useThreadContext();

  return (
    <p>
      {client.userID} is in {channel.cid}
      {thread ? `, in the thread of message ${thread.id}` : ""}
    </p>
  );
};
Hook Gives Provided by Outside its provider
useChatContext() client, channelManager, searchController, mutes Chat throws
useChannel() the channel: the thread's channel inside a Thread, otherwise the Channel's Channel or Thread throws
useChannelInstanceContext() { channel } Channel returns {}
useThreadContext() the thread Thread returns undefined
useMessageContext() the message, its handlers (handleDelete, handleReaction, ...) and display options for one message Message, once for each message a message list renders throws
useTranslationContext() t, tDateTimeParser, userLanguage Chat returns English defaults

useMessageContext() is how a custom message UI reads its message, because the SDK passes it no props. See A custom MessageUI.

The objects these hooks return hold their data in stores. Rendering from a store, and the rules that keep it current, are on Reading state in components.

Pitfalls

  • Passing a channel that was never watched. Channel renders what the channel holds and doesn't load it. Watch it first.
  • Reading the channel from your own state inside a thread. Read it with useChannel() and the thread with useThreadContext(), so the component works in both places.
  • Calling a throwing hook outside its provider. A component that renders both inside and outside a channel reads useChannelInstanceContext(), which returns {} instead of throwing.
  • Creating a thread with new Thread. Use client.threads.ensure(), so the thread is the one instance the client keeps up to date.

Used in: every page that renders state, including Channel UI, Thread UI and Message list UI.