# Channel list UI

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

The channel list is the screen of channels a user picks from. It is the UI of a `ChannelPaginator`: the paginator holds the query and the channels it loaded, and `ChannelList` renders them. This page covers what renders the list and its rows, what each row 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/v10/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 and a user is connected, it loads the first page. That happens when it mounts, when you pass it a different paginator, and after `client.disconnectUser()` resets the paginator. A paginator that already holds channels is shown as it is, without a new query.
2. It subscribes to `paginator.state` and renders a row 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 [Displaying notifications](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/displaying-notifications/).

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/v10/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/v10/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/v10/state/loading-data/channel-list-paginators/) explains.

## Rows

For each channel, the list renders a row that reads the channel's own stores, so a row re-renders only when its channel changes. The row 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 row 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 a sent, delivered or read mark on the user's own message, `ChannelPreviewMessageDeliveryStatus`. The mark reads `channel.messageReceiptsTracker.snapshotStore`.

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 row uses, such as `useChannelPreviewDisplayName`, are exported for rows of your own. See [Channel preview hooks](https://getstream.io/chat/docs/sdk/react-native/v10/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/v10/ui/customizing/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

| State                                       | What the list renders                                                   | Override key                              |
| ------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------- |
| Loading the first page                      | Placeholder rows, `numberOfSkeletons` of them (8 by default)            | `ChannelListLoadingIndicator`, `Skeleton` |
| Loading the next page                       | A spinner after the last row                                            | `ChannelListFooterLoadingIndicator`       |
| Refreshing                                  | The pull-to-refresh spinner above the rows                              | None                                      |
| No channels                                 | "No conversations yet". The component gets `listType="channel"`.        | `EmptyStateIndicator`                     |
| The first page failed                       | "Error loading channel list...". Tapping it loads the first page again. | `LoadingErrorIndicator`                   |
| A query failed while channels are on screen | A banner over the top of the list. Tapping it refreshes the list.       | `ChannelListHeaderErrorIndicator`         |
| No network, or the connection is down       | A "Reconnecting..." banner over the top of the list                     | `ChannelListHeaderNetworkDownIndicator`   |

A channel-list paginator retries a failed query three times. When every attempt has failed, the error is recorded as `lastQueryError` in `paginator.state`, and the list shows one of the error states above. With offline support on, a failure while the list has channels isn't recorded, so the channels from the local database stay on screen without an error.

`EmptyStateIndicator`, `LoadingErrorIndicator` and the network banner are shared with other parts of the SDK. The message list renders `EmptyStateIndicator` too, with `listType="message"`, so a replacement should check `listType`, as the sample below does.

## Replacing parts

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

| Override key                                                               | Replaces                                                                                                               |
| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `ChannelPreview`                                                           | The row's content. Gets `channel`, `muted`, `pinned`, `unread` and `lastMessage`. The default is `ChannelPreviewView`. |
| `ChannelPreviewAvatar`                                                     | The row'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/v10/ui/reading-state-in-components/).

This list keeps the default row, 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/v10/ui/reading-state-in-components/).

### The options sheet

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

| Prop                      | Type                                        | Description                                                                                                                                                                                                            |
| ------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channel`                 | `Channel`                                   | The row'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/v10/ui/customizing/actions/#channel-actions) explains the items. |
| `additionalFlatListProps` | `Partial<FlatListProps<ChannelActionItem>>` | Spread over the props of the list of actions, after its own. The row 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/v10/features/channels/channel-lists/)
- [Preview UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/channel-list/preview-ui/)
- [Pinning and archiving](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/channel-list/pinning-and-archiving/)
- [Channel backgrounds](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/channel-list/channel-list-background/)

---

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