ChannelList

ChannelList renders the channels of one ChannelPaginator, a row per channel, and loads the next page as the user scrolls. The paginator holds the query and the loaded channels; the list only renders them. Channel list UI explains what the list renders and reads, and Channel lists covers the feature end to end.

Rendering a list

Create a paginator, register it on client.channelManager, and pass it to ChannelList:

import { useEffect, useMemo } from "react";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import { ChannelList, useChatContext } from "stream-chat-react";

const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];

export const Inbox = ({ userId }: { userId: string }) => {
  const { client } = useChatContext();

  const paginator = useMemo(() => {
    const filters: ChannelFilters = { members: { $in: [userId] } };
    return new ChannelPaginator({ client, filters, id: "inbox", sort });
  }, [client, userId]);

  // A registered list is kept up to date as events arrive.
  useEffect(() => {
    client.channelManager.insertPaginator({ paginator });
    return () => {
      client.channelManager.removePaginator(paginator);
    };
  }, [client, paginator]);

  return <ChannelList paginator={paginator} />;
};

Filters, sort and registering are explained on Channel list paginators. Scope the filters to the user, as above: without that, a list can show channels the user isn't a member of.

Two components render every registered list for you. Neither takes props:

  • ChannelLists renders a ChannelList for each paginator on client.channelManager, in their order, and provides ChannelListContext.
  • ChannelNavigation renders the list header, the search bar, ChannelLists and the notifications of the channel-list panel. While a search is open, the results take the lists' place.

Outside ChatView, the list brings the str-chat element the design tokens are declared on. ChannelNavigation's root gets the str-chat and theme classes, and a ChannelList rendered on its own is wrapped in an element with them that has display: contents. Inside ChatView, neither adds one, and the list uses ChatView's. See Design tokens.

Selecting a channel

The list holds no selection. Clicking a row calls openChannel(channel, { event }) from useWorkspaceNavigation(), and the layout decides where the channel opens. A row is highlighted while isChannelActive(cid) is true for its channel. Without a layout, clicking a row does nothing; see Navigation.

To handle the click yourself, pass onSelect to the row; see Handle the row click yourself.

UI customization

Register replacements with WithComponents. ChannelList reads these keys:

Override key Replaces Gets
ListItem The whole row. The default renders ChannelListItem. item: the channel
FirstPageLoadingIndicator The placeholder rows while the first page loads nothing
ListLoadErrorIndicator The list, when its first page failed to load. The default shows "Couldn't load conversations" and a "Try again" button. ListLoadErrorIndicatorProps: error, isRetrying, retry
LoadingNextPageIndicator The spinner after the last row while the next page loads isLoading
EmptyListIndicator "No conversations yet", when the list has no channels nothing
EndReachedIndicator Nothing by default. Renders at the start and end of the list hasEnded, reached ("top" or "bottom")

The default row reads ChannelListItemUI, Avatar, SummarizedMessagePreview and ChannelListItemActionButtons; see Channel Preview.

import { ChannelList, WithComponents } from "stream-chat-react";

const NoChannels = () => <p className="no-channels">No conversations yet.</p>;

const overrides = { EmptyListIndicator: NoChannels };

export const ListWithEmptyState = () => (
  <WithComponents overrides={overrides}>
    <ChannelList paginator={paginator} />
  </WithComponents>
);

Props

Prop Type Default Description
paginator ChannelPaginator required The paginator whose channels the list renders. Register it on client.channelManager.
listboxProps ChannelListListboxProps none The accessible name of the list's role="listbox" element. Takes only aria-label and aria-labelledby. Without either, the list is named "Channel list", the channelList.channelList.ariaLabel translation. When both are set, aria-labelledby wins. Name each list when a page shows several.
loadMoreDebounceMs number 500 How long the list waits after the user stops scrolling before it checks whether to load the next page.
loadMoreThresholdPx number 250 How close to the end of the list, in pixels, scrolling has to come to load the next page.

The two load-more props trade responsiveness for work. A larger threshold starts the next page before the user reaches the end, so a fast scroll hits a spinner less often, but it loads pages the user may never see. A shorter debounce reacts sooner after the user stops scrolling, but checks the scroll position more often.

On mount, the list loads the first page if the paginator has no loaded page yet, and again whenever the paginator is reset to that state, as client.disconnectUser() does. Channels that events put into the paginator before its first query don't count as a page, and a page set with ChannelPaginator.setItems() does; see Registering paginators. It also starts the channel manager's event subscriptions, which every list shares. A failed first page isn't queried again on its own: see When the first page fails.

Everything else about a list is set on its paginator and on client.channelManager: filters, sort, page size, and how events move channels between lists. See Channel list paginators and Channel manager and ChannelPaginator.

Examples

Several lists

Register one paginator per list. <ChannelNavigation/> and <ChannelLists/> render them in order, and name every list "Channel list". To place them yourself and give each its own name, render one <ChannelList/> per paginator with listboxProps. Several lists has an example, and Which channels a list shows an inbox with an archive.

Handle the row click yourself

Register a ListItem that renders ChannelListItem with onSelect. The row then calls it instead of opening the channel:

import type { Channel } from "stream-chat";
import { ChannelListItem, WithComponents } from "stream-chat-react";
import type { PropsWithChildren } from "react";

const RowThatLogs = ({ item }: { item: unknown }) => {
  const channel = item as Channel;
  return (
    <ChannelListItem
      channel={channel}
      onSelect={() => console.log("Selected", channel.cid)}
    />
  );
};

const overrides = { ListItem: RowThatLogs };

export const WithLoggingRows = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

Hooks