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

ChatView components

ChatView is the SDK's layout. It decides where an opened channel or thread goes, and the components on this page render what it placed. Workspace with ChatView explains how they work together.

Everything on this page imports from stream-chat-react/slot-layout.

ChatView

The root of the layout. It holds the layout's state, provides workspace navigation to the components inside it, and renders the shown view. What it renders depends on its props:

  • With views: its children, then the shown view's content.
  • With layout="nav-rail-entity-list-workspace" and no views: the built-in workspace layout, the view selector in a rail, then every slot of the shown view rendered through slotRenderers. Its children aren't rendered.
  • With neither: its children.

Its root element has the classes str-chat, the theme from Chat, and str-chat__chat-view.

Prop Type Default Description
children ReactNode With views, rendered in every view, before the shown view's content: put components that must always run here, such as DisposedChannelRelease. With layout and no views, not rendered. With neither, the whole content.
deriveWorkspaceNavigation DeriveWorkspaceNavigation Replaces members of the navigation that ChatView provides. See Overriding navigation. Keep it stable.
dialogManagerId string a local manager Registers the dialog manager ChatView hosts inside its root under this id. Dialogs opened by the views' content render there, inside .str-chat. See Dialog managers.
duplicateEntityPolicy "allow" | "move" | "reject" "allow" What happens when a channel or thread is opened into a slot while another slot holds it. See Duplicates. A new value starts the layout again.
entityInferrers ChatViewEntityInferer[] No effect: ChatView accepts it, but nothing reads it.
layout "nav-rail-entity-list-workspace" Renders the built-in workspace layout. Ignored when views is passed.
layoutController LayoutController one ChatView creates A layout controller to use instead. layouts, duplicateEntityPolicy and resolveDuplicateEntity set up the controller ChatView creates, so they have no effect with this prop. The LayoutController class isn't exported, only its type.
layouts LayoutDescriptor[] one slot per view, slot1, holding its list Each view's slots, in order, and what they hold at the start. See LayoutDescriptor. Without views or layout, the default is one empty slot1, in the channels view only. A new array starts the layout again.
resolveDuplicateEntity ResolveDuplicateEntity Decides the duplicate policy per open, instead of duplicateEntityPolicy. A new function starts the layout again.
SlotFallback ComponentType<{ slot: string }> Built-in layout only. Rendered in a slot that holds nothing, or whose renderer returns nothing. Without it, such a slot is blank, and while every slot is blank the layout shows ChatViewEmptyPlaceholder.
slotFallbackComponents Partial<Record<string, ComponentType<{ slot: string }>>> Built-in layout only. SlotFallback per slot name. Wins over SlotFallback.
slotRenderers ChatViewSlotRenderers renderers for channelList, threadList and userProfile How Slot and the built-in layout render each kind of thing a slot holds. See slotRenderers. Keep it stable.
viewActionSlotResolvers Partial<Record<ChatView, ViewActionSlotResolvers>> Picks the slot for an opened channel or thread, per view. See viewActionSlotResolvers. Keep it stable.
views { channels?: ReactNode; threads?: ReactNode } The content of each view. The shown view's content renders in a div with the class str-chat__chat-view__channels or str-chat__chat-view__threads. The views are channels and threads only: the ChatView type is "channels" | "threads".

"Keep it stable" means declaring the value outside the component, or memoizing it. Views and slots explains why layouts has to be stable too.

useChatViewContext

useChatViewContext() reads the ChatView around the component. It throws outside a ChatView.

Member Type Description
activeView "channels" | "threads" The view that's shown.
setActiveView (view: ChatView) => void Shows a view. Each view keeps what its slots hold.
layoutController LayoutController The layout's state and the methods that change it. See The layout controller.
registerViewActionSlotResolvers (view: ChatView, resolvers?: ViewActionSlotResolvers) => void Sets the slot resolvers of a view, as the viewActionSlotResolvers prop does. undefined removes them.
resolveActionTargetSlot (view: ChatView, args: ResolveViewActionTargetSlotArgs) => string | undefined Runs the view's resolver for args.action.
entityInferers ChatViewEntityInferer[] The entityInferrers prop. Nothing reads it.

ChatViewContext itself is exported too. useContext(ChatViewContext) returns undefined outside a ChatView instead of throwing, which suits a component that also renders without one.

Switching between the channel list and the thread list has an example of setActiveView. For opening and closing channels and threads, use useWorkspaceNavigation() instead: it works with any layout.

DisposedChannelRelease

DisposedChannelRelease empties every slot of the shown view whose channel stream-chat disposed of: deleted, or the current user removed from it. It takes no props and renders nothing. It's opt-in: render it once inside ChatView, as one of its children. The built-in layout doesn't render ChatView's children, so it can't host it. Disposed channels explains when a channel is disposed of.

WorkspaceLayout

A frame of slots: an optional rail, then a row of slots. The built-in workspace layout is a WorkspaceLayout, and a view's content can use one too.

Prop Type Default Description
slots WorkspaceLayoutSlot[] required The slots, in order. Each is { slot: string; content?: ReactNode }, rendered as a Slot with content as its children.
navRail ReactNode Rendered in the rail, str-chat__chat-view__workspace-layout-nav-rail, before the slots. Without it, there's no rail.

While no entry has content that renders something (every one is undefined, null or a boolean), WorkspaceLayout shows ChatViewEmptyPlaceholder in place of the slots. So give each slot its content. An entry without content, next to one with content, renders what its slot holds through slotRenderers.

import {
  ChannelHeader,
  ChannelNavigation,
  MessageComposer,
  MessageList,
  ThreadHeader,
  ThreadList,
} from "stream-chat-react";
import {
  ChannelSlot,
  ChatView,
  ChatViewEmptyPlaceholder,
  ThreadSlot,
  WorkspaceLayout,
} from "stream-chat-react/slot-layout";
import type { LayoutDescriptor } from "stream-chat-react/slot-layout";

// "list" isn't one of the views' slots, so nothing opens there.
const layouts: LayoutDescriptor[] = [
  { id: "channels", slots: ["channel", "thread"] },
  { id: "threads", slots: ["thread"] },
];

const threadPanel = {
  content: (
    <ThreadSlot slot="thread">
      <ThreadHeader />
      <MessageList />
      <MessageComposer />
    </ThreadSlot>
  ),
  slot: "thread",
};

const ChannelsView = () => (
  <WorkspaceLayout
    navRail={<ChatView.Selector />}
    slots={[
      { content: <ChannelNavigation />, slot: "list" },
      {
        content: (
          <ChannelSlot fallback={<ChatViewEmptyPlaceholder />} slot="channel">
            <ChannelHeader />
            <MessageList />
            <MessageComposer />
          </ChannelSlot>
        ),
        slot: "channel",
      },
      threadPanel,
    ]}
  />
);

const ThreadsView = () => (
  <WorkspaceLayout
    navRail={<ChatView.Selector />}
    slots={[{ content: <ThreadList />, slot: "list" }, threadPanel]}
  />
);

export const Workspace = () => (
  <ChatView
    layouts={layouts}
    views={{ channels: <ChannelsView />, threads: <ThreadsView /> }}
  />
);

Each view gets its content: the selector can show either view, and a view without content renders nothing, the selector included. The channels view has a thread slot so that a reply thread opens beside the channel. With only a channel slot, the thread would replace the channel there, and ChannelSlot would show its fallback.

ChatViewEmptyPlaceholder

The SDK's empty state for a layout: an icon and "No chat selected", or "No thread selected" in the threads view. It takes no props. WorkspaceLayout shows it while no slot has content, and it works as a fallback or SlotFallback. Outside a ChatView, it uses the channels wording.

Slot

Renders what one slot holds, and the layers on top of it, through slotRenderers.

Prop Type Default Description
slot string required The slot to render.
children ReactNode Rendered instead of what the slot holds. Layers still render on top of it.
className string Added to the slot element.

It renders a section with the class str-chat__chat-view__slot and data-slot set to the slot name. A hidden slot also gets str-chat__chat-view__slot--hidden and aria-hidden, and stays mounted. While the slot has layers, what it holds and each layer stay mounted: only the top one shows, and the ones beneath get the hidden attribute. Pushing the first layer and removing the last one mount what the slot holds again, so it loses its scroll position.

Slot declares its slot as the panel its content renders in, so useWorkspacePanel() inside returns the slot name. Panels explains what the panel is for.

Entity adapters

Three components render a slot's channel, thread or thread list with the matching SDK component, around their children. Rendering a slot compares them with Slot.

Always pass slot. Without it, ChannelSlot and ThreadSlot render the first channel or thread in the view, which is only right in a layout with one of each. An adapter only displays: it never moves a channel or thread from another slot.

ChannelSlot

Renders a Channel for the channel in its slot. It shows what the slot holds underneath any layer, never the layer.

Prop Type Default Description
slot string The slot to read.
fallback ReactNode null Rendered while the slot holds no channel.
children ReactNode Rendered inside the Channel.

It also takes every prop of Channel except channel, and passes them on.

ThreadSlot

Renders a Thread for the thread in its slot. Like ChannelSlot, it shows what the slot holds underneath any layer.

Prop Type Default Description
slot string The slot to read.
fallback ReactNode null Rendered while the slot holds no thread.
hideIfEmpty boolean true Hides the slot while it holds no thread, and shows it again when one opens. Needs slot.
children ReactNode Rendered inside the Thread.

It also takes every prop of Thread except thread. ThreadSlot doesn't mark the thread as on screen: call useActiveThread inside it.

ThreadListSlot

Puts a thread list in a slot of the shown view, and renders that slot as a Slot. The slot it uses is the first of: slot, when the view has it; the slot that already holds a thread list; the first empty slot; the view's first slot. When slot is passed and another slot holds the list, it empties that one.

Prop Type Default Description
slot string The slot to put the thread list in.
fallback ReactNode null Rendered when the view has no slot. While children aren't passed, it's also rendered in place of the thread list.
children ReactNode Rendered in the slot instead of the thread list.

Without children or fallback, the slot renders slotRenderers.threadList, which is ThreadList by default.

View selector

ChatView.Selector renders the buttons that switch views, in a role="navigation" element labelled "Chat view controls".

Prop Type Default Description
iconOnly boolean true Shows icons without their labels. Passed to each button.
itemSet ChatViewSelectorEntry[] defaultChatViewSelectorItemSet The buttons, in order.

Each entry is { Component, type }: Component receives iconOnly, and type is the entry's unique key. The default set is the channels button and the threads button. A button of your own doesn't have to switch views: there are only the two, channels and threads. This set adds a button that opens a support channel:

import { useMemo } from "react";
import { useChatContext, useWorkspaceNavigation } from "stream-chat-react";
import {
  ChatView,
  ChatViewSelectorButton,
  defaultChatViewSelectorItemSet,
} from "stream-chat-react/slot-layout";
import type {
  ChatViewSelectorEntry,
  ChatViewSelectorItemProps,
} from "stream-chat-react/slot-layout";

const SupportIcon = () => (
  <svg aria-hidden="true" viewBox="0 0 20 20">
    <circle cx="10" cy="10" fill="currentColor" r="8" />
  </svg>
);

const SupportSelectorButton = ({ iconOnly }: ChatViewSelectorItemProps) => {
  const { client } = useChatContext();
  const { isChannelActive, openChannel } = useWorkspaceNavigation();
  const channel = useMemo(
    () => client.channel("messaging", "support"),
    [client],
  );

  return (
    <ChatViewSelectorButton
      Icon={SupportIcon}
      iconOnly={iconOnly}
      isActive={isChannelActive(channel.cid)}
      onClick={(event) => {
        // openChannel places the channel; watching loads it.
        channel.watch().catch((error) => console.error(error));
        openChannel(channel, { event });
      }}
      text="Support"
    />
  );
};

const itemSet: ChatViewSelectorEntry[] = [
  ...defaultChatViewSelectorItemSet,
  { Component: SupportSelectorButton, type: "support" },
];

export const Selector = () => <ChatView.Selector itemSet={itemSet} />;

The individual buttons

Render them directly when a rail of your own needs its own arrangement.

Component Props Description
ChatViewChannelsSelectorButton iconOnly (true) Shows the channels view. Its icon is filled while that view is shown.
ChatViewThreadsSelectorButton iconOnly (true) Shows the threads view, with a badge counting the threads that have unread replies. Its aria-label includes the count.
ChatViewSelectorButton see below The button both are built on.

ChatViewSelectorButton takes every prop of a button, plus:

Prop Type Default Description
Icon ComponentType The icon.
ActiveIcon ComponentType The icon while isActive. Falls back to Icon.
isActive boolean Marks the button as the current one, with aria-current.
text string The label. With iconOnly, it's shown as a tooltip and used as the aria-label.
iconOnly boolean true Hides the label next to the icon.
children ReactNode Rendered instead of the icon.

useActiveThread

useActiveThread({ activeThread }) marks a thread as on screen while the window is visible and focused, and unmarks it when the window is hidden or loses focus, when the component unmounts, and when activeThread changes. It returns nothing.

Option Type Description
activeThread Thread | undefined The thread on screen. undefined does nothing.

While a thread is on screen, its new replies are marked read, and a thread that went out of date, after a lost connection for example, reloads. ThreadSlot doesn't call it, so call it from a component inside the slot:

const ActiveThread = () => {
  const thread = useThreadContext();
  useActiveThread({ activeThread: thread });
  return null;
};

It doesn't need ChatView: Providing your own navigation calls it without one.

See also