# Channel list UI

The channel list is the screen of channels a user picks from. It is the UI of a `ChannelPaginator`: the paginator holds the list's filters and sort, and the channels it loaded, and `ChannelList` renders them. Each row of the list renders a **channel preview**: the channel's title, image, latest message and status. This page covers what renders the list and its channel previews, what each channel preview reads, the swipe actions, and which parts you can replace. The props are in the [ChannelList reference](https://getstream.io/chat/docs/sdk/react-native/component-reference/channel-list/channellist/).

## The ChannelList component

`<ChannelList paginator={paginator} onSelect={onSelect} />` renders the channels of one paginator:

1. When the paginator has no channels loaded, or was never queried, and a user is connected, it loads the first page, even when events already put channels in the paginator. That happens when it mounts, when you pass it a different paginator, and after `client.disconnectUser()` resets the paginator. A paginator that was queried and holds channels is shown as it is, without a new query. After a reconnect, `client.connectionRecovery` re-queries the channels the list has loaded, up to 150 per list; see [Channel list paginators](https://getstream.io/chat/docs/sdk/react-native/state/loading-data/channel-list-paginators/).
2. It subscribes to `paginator.state` and renders a channel preview for each channel, in a React Native `FlatList`.
3. When the user scrolls near the end, it loads the next page. `loadMoreThreshold` sets how near, as the `FlatList`'s `onEndReachedThreshold`.
4. It starts the event subscriptions of `client.channelManager`, so the list stays up to date as messages and other events arrive. Every mounted list shares one set of subscriptions, which stops when the last list unmounts.
5. Pulling the list down refreshes it: the first page is queried again while the loaded channels stay on screen. A pull within 5 seconds of the last refresh, or of the list mounting, is ignored, unless the last query failed.
6. It renders a `NotificationList` for the notifications aimed at the channel list. See [Notifications UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/notifications-ui/).

The list holds no selection and doesn't navigate. Tapping a row calls `onSelect(channel)`, and your navigator shows the channel; see [Navigation](https://getstream.io/chat/docs/sdk/react-native/ui/layout/navigation/). While a row's swipe actions are open, a tap closes them instead.

The paginator belongs to your app. Register it on `client.channelManager` so it receives events: an unregistered paginator shows its first page and then never changes. `ChannelList` never disposes the paginator. [Channel list paginators](https://getstream.io/chat/docs/sdk/react-native/state/loading-data/channel-list-paginators/) covers creating, registering and disposing one.

`channelRenderFilterFn` filters the loaded channels before they render. It runs on every render of the list, so keep it a cheap pure function.

### Several lists

The SDK has no component that renders every registered list. Render one `ChannelList` per paginator. On a phone, one list at a time usually fits, so this screen switches between an "Unread" list and the full list:

```tsx title="src/ChannelListScreen.tsx"
import { useEffect, useMemo, useState } from "react";
import { Button, StyleSheet, View } from "react-native";
import { ChannelPaginator } from "stream-chat";
import type { Channel, ChannelFilters, SortParamRequest } from "stream-chat";
import { ChannelList, useChatContext } from "stream-chat-react-native";

const userId = "YOUR_USER_ID";

const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];
const allFilters: ChannelFilters = {
  members: { $in: [userId] },
  type: "messaging",
};
const unreadFilters: ChannelFilters = { ...allFilters, has_unread: true };

export const ChannelListScreen = ({
  onSelect,
}: {
  onSelect: (channel: Channel) => void;
}) => {
  const { client } = useChatContext();
  const [tab, setTab] = useState<"all" | "unread">("all");

  const [unread, all] = useMemo(
    () => [
      new ChannelPaginator({
        client,
        filters: unreadFilters,
        id: "unread",
        sort,
      }),
      new ChannelPaginator({ client, filters: allFilters, id: "all", sort }),
    ],
    [client],
  );

  // Registered lists are kept up to date by the channel manager.
  useEffect(() => {
    client.channelManager.insertPaginator({ paginator: unread });
    client.channelManager.insertPaginator({ paginator: all });
    return () => {
      client.channelManager.removePaginator(unread);
      client.channelManager.removePaginator(all);
      unread.dispose();
      all.dispose();
    };
  }, [all, client, unread]);

  return (
    <View style={styles.container}>
      <View style={styles.tabs}>
        <Button onPress={() => setTab("all")} title="All" />
        <Button onPress={() => setTab("unread")} title="Unread" />
      </View>
      <ChannelList
        onSelect={onSelect}
        paginator={tab === "all" ? all : unread}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 },
  tabs: { flexDirection: "row", justifyContent: "center" },
});
```

Both paginators are registered, so both stay up to date while the other one is on screen. Switching tabs shows the other paginator's channels without a new query once it has loaded. A channel can match both lists; the channel manager decides which lists keep it, as [Channel list paginators](https://getstream.io/chat/docs/sdk/react-native/state/loading-data/channel-list-paginators/) explains.

## Channel previews

For each channel, the list renders a channel preview. It reads the channel's own stores, so a channel preview re-renders only when its channel changes. It computes the channel's muted, pinned and unread values and its latest message, and renders the `ChannelPreview` override, by default `ChannelPreviewView`, with them as props. The default is made of parts, each its own override key:

| The channel preview shows                          | Rendered by                                               | Read from                                                                  |
| -------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------- |
| Avatar: the channel image, or its members' avatars | `ChannelPreviewAvatar`                                    | `channel.state`: the channel data and members                              |
| Title                                              | `ChannelPreviewTitle`                                     | `channel.state`: the channel's name, or else the other members' names      |
| Muted and pinned icons                             | `ChannelPreviewMutedStatus`, `ChannelPreviewPinnedStatus` | `channel.state`: the mute status, and the user's membership (`pinned_at`)  |
| Time of the latest message                         | `ChannelPreviewStatus`                                    | `channel.messagePaginator.aggregateState`: the latest message              |
| Unread badge                                       | `ChannelPreviewUnreadCount`                               | `channel.state`: the user's read state. Hidden while the channel is muted. |
| The message line                                   | `ChannelPreviewMessage`                                   | See below                                                                  |

The title is the channel's name when it has one. Without one, a direct message shows the other member's name, and a group shows the first names of two other members, followed by "and N others" when there are more.

The unread badge caps the count at `maxUnreadCount`, 255 by default.

`ChannelPreviewMessage` shows the first of these that applies:

1. Who is typing, from `channel.state.typing`, through `ChannelPreviewTypingIndicator`.
2. "Draft:" and what the channel's composer holds, when the user left text or attachments in it.
3. "No messages yet", when the channel has no messages.
4. The poll's label, when the latest message is a poll.
5. "Message failed to send", when the user's latest message failed.
6. Otherwise the latest message, through `ChannelPreviewLastMessage`, after who sent it, `ChannelPreviewMessageDeliveryStatus`. The user's own message gets "You:" with a sending, sent, delivered or read mark, read from `channel.messageReceiptsTracker.snapshotStore`. A message from another member gets that member's name, except in a direct message. A deleted message gets the same sender, and no mark. When the channel has read events turned off, only the sending mark shows.

The latest message is translated into the user's language when it has a translation for it.

`mutedStatusPosition` and `pinnedStatusPosition` place the muted and pinned icons next to the title (`'inlineTitle'`, the default) or at the end of the message line (`'trailingBottom'`).

The channel preview hooks the default uses, such as `useChannelPreviewDisplayName`, are exported for channel previews of your own. See [Channel preview hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/channel-preview-hooks/).

## Swipe actions

Swiping a row toward the start reveals its actions. By default there are two:

- **A "more" button** that opens a bottom sheet, `ChannelDetailsBottomSheet`, with the channel's avatar, name and member count (online status in a direct message) at the top, and a list of actions.
- **Mute or unmute**, as a button in the swipe row itself.

The sheet lists pin or unpin, and leave. In a direct message it also lists mute user and block user. The user who created the channel also gets delete. Leaving, deleting and blocking ask for confirmation first.

Each action has a `placement`: `'swipe'` shows it in the swipe row, `'sheet'` in the bottom sheet, and `'both'` in both. To change the actions, pass `getChannelActionItems` to `ChannelList`: it gets the default items and returns the list to show. [Actions](https://getstream.io/chat/docs/sdk/react-native/ui/components/actions/) shows how. `swipeActionsEnabled={false}` turns swiping off; the rows then have no actions.

Opening a row's actions closes the actions of any other open row.

## Loading, empty and error states

`ChannelList` shows a state of its own while the first page loads, when it fails, while the next page loads, and when it has no channels. [List states UI](https://getstream.io/chat/docs/sdk/react-native/ui/components/list-states-ui/) lists each state's default and override key, and explains what happens when a query fails.

## Replacing parts

Register replacements with `WithComponents`. [Overriding components](https://getstream.io/chat/docs/sdk/react-native/ui/components/overriding-components/) explains how overrides apply.

| Override key                                                               | Replaces                                                                                                                 |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `ChannelPreview`                                                           | The channel preview. Gets `channel`, `muted`, `pinned`, `unread` and `lastMessage`. The default is `ChannelPreviewView`. |
| `ChannelPreviewAvatar`                                                     | The channel preview's avatar. The default is `ChannelAvatar`.                                                            |
| `ChannelPreviewTitle`                                                      | The title                                                                                                                |
| `ChannelPreviewStatus`                                                     | The time of the latest message                                                                                           |
| `ChannelPreviewUnreadCount`                                                | The unread badge                                                                                                         |
| `ChannelPreviewMutedStatus`, `ChannelPreviewPinnedStatus`                  | The muted and pinned icons                                                                                               |
| `ChannelPreviewMessage`                                                    | The whole message line                                                                                                   |
| `ChannelPreviewLastMessage`                                                | The latest message, or the draft, in the message line                                                                    |
| `ChannelPreviewMessageDeliveryStatus`                                      | The sent, delivered or read mark                                                                                         |
| `ChannelPreviewTypingIndicator`                                            | Who is typing, in the message line                                                                                       |
| `ChannelDetailsBottomSheet`, `ChannelDetailsHeader`                        | The bottom sheet the "more" button opens, and the header at its top                                                      |
| `ListHeaderComponent`                                                      | Nothing by default. Rendered above the first row, and scrolls with the list.                                             |
| `ChannelListLoadingIndicator`, `Skeleton`                                  | The first-page placeholder, and each placeholder row in it                                                               |
| `ChannelListFooterLoadingIndicator`                                        | The next-page spinner                                                                                                    |
| `EmptyStateIndicator`, `LoadingErrorIndicator`                             | The empty and first-page error states, here and in the message list                                                      |
| `ChannelListHeaderErrorIndicator`, `ChannelListHeaderNetworkDownIndicator` | The error and connection banners                                                                                         |

`ChannelList` itself has no override key. To render the channels in a list of your own, read `paginator.state` with `useStateStore` and render rows from it; see [Reading state in components](https://getstream.io/chat/docs/sdk/react-native/ui/reading-state-in-components/).

This list keeps the default channel preview, adds a label to frozen channels, and replaces the empty state of the channel list only:

```tsx title="src/ChannelListWithLabels.tsx"
import { StyleSheet, Text, View } from "react-native";
import type { Channel, ChannelPaginator, ChannelStateData } from "stream-chat";
import type {
  ChannelPreviewViewProps,
  EmptyStateProps,
} from "stream-chat-react-native";
import {
  ChannelList,
  ChannelPreviewView,
  EmptyStateIndicator,
  useStateStore,
  WithComponents,
} from "stream-chat-react-native";

const frozenSelector = ({ data }: ChannelStateData) => ({
  frozen: data?.frozen ?? false,
});

const RowWithFrozenLabel = (props: ChannelPreviewViewProps) => {
  const { frozen } = useStateStore(props.channel.state, frozenSelector);

  return (
    <View>
      <ChannelPreviewView {...props} />
      {frozen ? <Text style={styles.label}>Frozen</Text> : null}
    </View>
  );
};

// The message list renders this override too, so only the channel list's state is replaced.
const EmptyState = (props: EmptyStateProps) =>
  props.listType === "channel" ? (
    <Text style={styles.empty}>You aren't in any channels yet.</Text>
  ) : (
    <EmptyStateIndicator {...props} />
  );

// Declared outside the component, so WithComponents gets the same object on every render.
const overrides = {
  ChannelPreview: RowWithFrozenLabel,
  EmptyStateIndicator: EmptyState,
};

export const ChannelListWithLabels = ({
  onSelect,
  paginator,
}: {
  onSelect: (channel: Channel) => void;
  paginator: ChannelPaginator;
}) => (
  <WithComponents overrides={overrides}>
    <ChannelList onSelect={onSelect} paginator={paginator} />
  </WithComponents>
);

const styles = StyleSheet.create({
  empty: { padding: 16, textAlign: "center" },
  label: { fontSize: 12, paddingHorizontal: 16, paddingBottom: 8 },
});
```

The label reads `frozen` from `channel.state` through `useStateStore`, so it updates when the channel is frozen or unfrozen. Reading `channel.data.frozen` during render would not: the getter isn't reactive. See [Reading state in components](https://getstream.io/chat/docs/sdk/react-native/ui/reading-state-in-components/).

### The options sheet

The channel preview renders the `ChannelDetailsBottomSheet` key inside the bottom sheet that the "more" button opens. It receives `ChannelDetailsBottomSheetProps`:

| Prop                      | Type                                        | Description                                                                                                                                                                                                       |
| ------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channel`                 | `Channel`                                   | The channel preview's channel.                                                                                                                                                                                    |
| `items`                   | `ChannelActionItem[]`                       | The actions whose `placement` is `'sheet'` or `'both'`, from the list `getChannelActionItems` returned. [Channel actions](https://getstream.io/chat/docs/sdk/react-native/ui/components/actions/#channel-actions) explains the items. |
| `additionalFlatListProps` | `Partial<FlatListProps<ChannelActionItem>>` | Spread over the props of the list of actions, after its own. The channel preview doesn't pass it, so only a replacement that renders the default sheet sets it.                                                   |

The default sheet renders the `ChannelDetailsHeader` key with the channel, then the items in a list. Each item is a `ChannelActionItemView`, which runs the item's `action`, closes the sheet and closes every open row's swipe actions when pressed.

`ChannelDetailsHeader` receives `ChannelDetailsHeaderProps`, which hold `channel` only. The default shows the channel's avatar, its name with the muted icon, and the member and online counts, or "Online" or "Offline" in a direct message.

This sheet keeps the default, and shows a line when `getChannelActionItems` leaves it no items:

```tsx title="src/ChannelOptionsSheet.tsx"
import type { FlatListProps } from "react-native";
import { Text } from "react-native";
import { ChannelDetailsBottomSheet } from "stream-chat-react-native";
import type {
  ChannelActionItem,
  ChannelDetailsBottomSheetProps,
  ComponentOverrides,
} from "stream-chat-react-native";

// Declared once, so the list gets the same props on every render.
const listProps: Partial<FlatListProps<ChannelActionItem>> = {
  ListEmptyComponent: <Text style={{ padding: 16 }}>No actions</Text>,
};

const ChannelOptionsSheet = (props: ChannelDetailsBottomSheetProps) => (
  <ChannelDetailsBottomSheet {...props} additionalFlatListProps={listProps} />
);

// Pass to WithComponents, declared once at module scope.
export const sheetOverrides: ComponentOverrides = {
  ChannelDetailsBottomSheet: ChannelOptionsSheet,
};
```

## Used in

- [Channel lists](https://getstream.io/chat/docs/sdk/react-native/features/channels/channel-lists/)
- [Preview UI](https://getstream.io/chat/docs/sdk/react-native/ui-cookbooks/channel-list/preview-ui/)
- [Pinning and archiving](https://getstream.io/chat/docs/sdk/react-native/ui-cookbooks/channel-list/pinned-and-archived-channels/)
- [Channel backgrounds](https://getstream.io/chat/docs/sdk/react-native/ui-cookbooks/channel-list/channel-list-background/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/ui/components/channel-list-ui/](https://getstream.io/chat/docs/sdk/react-native/ui/components/channel-list-ui/).