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 thei18nInstanceyou 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.pollsandclient.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__channelelement, around its children. To fill the same place while no channel is selected, renderChannelPlaceholder.
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
Channelaround it. Inside aThread,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.
Channelrenders 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 withuseThreadContext(), 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. Useclient.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.