Providers and context hooks

Components in the UI layer don't fetch or keep chat data. They read it from objects in the state layer, and get those objects in two ways:

  • From the nearest provider above them. <Chat/> provides the client, <Channel/> provides a channel, and <Thread/> provides a thread. Inside those, narrower providers provide one item: each message in a message list provides its message, a poll provides its poll, and each channel list row provides its channel.
  • As a prop. <ChannelList/> renders the ChannelPaginator you pass it, and <Channel/> and <Thread/> take the channel and thread they provide.

Use this page to know which channel or thread a component works on, and to read the same objects in your own components.

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 <MessageList/> elements 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 <MessageComposer/> elements 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:

What Read it with
the client, its channel manager, the search controller, the users the current user has muted, and the theme and class names useChatContext()
translations, from the i18nInstance you pass or a default English one useTranslationContext()
the dialog manager for modals, and the screen-reader announcer the SDK's dialogs and components
the display filter that decides where notifications appear the notification lists, see Notifications UI
voice recording playback, so one recording plays at a time the voice recording players

Every prop is on Chat, and every field of the context on ChatContext.

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: call SearchController.registerSubscriptions() when it mounts and SearchController.dispose() when it unmounts, as <Chat/> does for its own. Choosing what to search has a sample controller that searches only channels and users.

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 Returns Provided by Outside its provider
useChatContext() client, channelManager, searchController, mutes and more, see ChatContext <Chat/> throws
useChannel() the channel: the thread's channel inside a <Thread/>, otherwise the <Channel/>'s <Channel/> or <Thread/> throws
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
useChannelListItemContext() { channel }, the channel of one channel list row each row a channel list renders throws
usePollContext() { poll } <Poll/>, for a message that carries a poll throws
useMessageComposerController() the composer: the thread's inside a <Thread/>, otherwise the channel's, or the one a MessageComposerControllerProvider was given <Channel/>, <Thread/> or MessageComposerControllerProvider throws
useMessagePaginator() the message paginator: the thread's inside a <Thread/>, otherwise the channel's <Channel/> or <Thread/> throws

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

Which owner a hook reads

The context hooks find their owner from where the component sits, so the same component works in a channel and in a thread:

Hook Inside a <Channel/> Inside a <Thread/>
useChannel() the channel the thread's channel
useThreadContext() undefined the thread
useMessageComposerController() channel.messageComposer thread.messageComposer
useMessagePaginator() channel.messagePaginator thread.messagePaginator

Below a MessageComposerControllerProvider, useMessageComposerController() returns the composer that provider was given instead, for example the one editing a message inline, as in Inline message editing.

useMessagePaginator() picks the message paginator the way the SDK's own message lists do: the thread's when there is one, otherwise the channel's.

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's composer or messages inside a thread. Inside a <Thread/>, useChannel() returns the thread's channel, so channel.messageComposer and channel.messagePaginator are the channel's, not the thread's. Read the composer with useMessageComposerController(), the message paginator with useMessagePaginator(), and the thread with useThreadContext(), so the component works in both places.
  • Calling a throwing hook outside its provider. useChannel() throws outside a <Channel/> or a <Thread/>, and so do the hooks built on it, such as useMessageComposerController() and useChannelCapabilities(). useChatContext() throws outside <Chat/>. Render the component inside the provider. In a channel list row, read the row's channel with useChannelListItemContext().
  • 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: