# Channel list paginators

> **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/).

A `ChannelPaginator` holds the channels of a channel list. It is a query for channels, with filters, a sort and request options, and the channels it has loaded. It is the one kind of paginator you create yourself, and `ChannelList` renders it. Registered on `client.channelManager`, it stays current as messages and other events arrive.

An app can have several.

>
> **Info:** **Example**
>
> The user's app shows their pinned channels, their inbox and their archive, each a `ChannelPaginator` with its own filters. One manager keeps all three current from one set of event listeners, and moves a channel from one paginator to another when it changes.
>

This page builds on [How pagination works](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/how-pagination-works/), which explains loading, staying live and ordering for every paginator. Rendering a paginator is on [Channel list UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/channel-list-ui/).

## Creating a channel paginator

```ts
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest, StreamChat } from "stream-chat";

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

export const createInbox = (client: StreamChat, userId: string) => {
  const inboxFilters: ChannelFilters = {
    archived: false,
    members: { $in: [userId] },
    type: "messaging",
  };

  return new ChannelPaginator({
    client,
    filters: inboxFilters,
    id: "inbox",
    paginatorOptions: { pageSize: 30 },
    requestOptions: { message_limit: 25, presence: true },
    sort: latestFirst,
  });
};
```

The options:

- **`id`** names the paginator. Ownership rules and `ChannelManager.getPaginatorById()` use it. Without one, the paginator gets a random id.
- **`filters`** are sent with every page, and the client matches channels against them as events arrive.
- **`sort`** orders the channels. The default is `last_message_at`, then `updated_at`, both newest first.
- **`requestOptions`** go with every page request: `message_limit`, `member_limit`, `presence`, `state`, `watch` and the other `queryChannels` options. `limit` and `offset` are left out, because the paginator sets them.
- **`paginatorOptions`** tune the paginator: `pageSize` (10 by default), `debounceMs`, `retryCount` (3 by default), `lockItemOrder` and `throwErrors`.

Each page is a `queryChannels` request. It watches the channels it returns, unless `requestOptions.watch` is `false`.

Create a paginator once for as long as its screen needs it, for example in a `useMemo` keyed on the client, not on every render. `ChannelList` loads the first page of a paginator that has none, so a new paginator on every render queries again each time.

## Registering paginators

A paginator stays current only while it's registered on `client.channelManager`. Register your paginators with `ChannelManager.setPaginators()`, in the order the UI should show them:

```ts
import { ChannelPaginator } from "stream-chat";
import type { StreamChat } from "stream-chat";

export const registerPaginators = (client: StreamChat, userId: string) => {
  const mine = { members: { $in: [userId] } };

  // Replaces the whole set in one update, in this order. A registered paginator left out is
  // removed, and a repeated id keeps only its first occurrence.
  client.channelManager.setPaginators([
    new ChannelPaginator({
      client,
      filters: { ...mine, pinned: true },
      id: "pinned",
    }),
    new ChannelPaginator({
      client,
      filters: { ...mine, archived: false },
      id: "inbox",
    }),
    new ChannelPaginator({
      client,
      filters: { ...mine, archived: true },
      id: "archive",
    }),
  ]);
};
```

To add or remove one paginator at a time, use `ChannelManager.insertPaginator()` and `ChannelManager.removePaginator()`:

```ts
import { ChannelPaginator } from "stream-chat";
import type { StreamChat } from "stream-chat";

export const registerInboxAndArchive = (client: StreamChat, userId: string) => {
  const inbox = new ChannelPaginator({
    client,
    filters: { archived: false, members: { $in: [userId] } },
    id: "inbox",
  });
  const archive = new ChannelPaginator({
    client,
    filters: { archived: true, members: { $in: [userId] } },
    id: "archive",
  });

  // Adds a paginator at `index`, or at the end without one. A paginator with the same id
  // moves to the new position.
  client.channelManager.insertPaginator({ paginator: inbox });
  client.channelManager.insertPaginator({ paginator: archive, index: 0 });

  // Returns the registered paginator with that id, the same object, not a copy.
  const registeredArchive = client.channelManager.getPaginatorById("archive");
  console.log(registeredArchive === archive); // true

  // When the paginators are no longer shown. A removed paginator stops getting event updates
  // and keeps the channels it loaded, so it could be registered again. These two are done for
  // good, so `dispose()` lets go of their channels.
  return () => {
    client.channelManager.removePaginator(inbox);
    client.channelManager.removePaginator(archive);
    inbox.dispose();
    archive.dispose();
  };
};
```

`client.channelManager.state` holds the registered paginators as `paginators`, in their registration order, and publishes when one is registered, moved or removed. `client.channelManager.paginators` reads the current set once. The SDK has no component that renders all of them. Render one `ChannelList` for each paginator you show. A screen usually registers its paginators in an effect and removes them when it unmounts, as the sample on [Channel list UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/channel-list-ui/#several-lists) does.

A paginator you're done with for good also needs `paginator.dispose()`. A paginator holds the channels it loaded, so they count as used until it lets go of them. `ChannelPaginator.dispose()` does that, and the [cleanup of unused channels](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/entity-lifecycle/#cleaning-up-unused-channels) can then drop them. `ChannelList` never disposes a paginator.

Registering a paginator loads nothing. The paginator loads its first page when something calls `ChannelPaginator.toTail()` or `ChannelPaginator.reload()`. `ChannelList` calls `ChannelPaginator.reload()` whenever its paginator has nothing loaded and a user is connected, for example when it first mounts or when it's given a new paginator. The manager handles events only while its subscriptions are registered. Each mounted `ChannelList` registers them, and they stop when the last one unmounts. Code that keeps paginators current while no `ChannelList` is mounted calls `client.channelManager.registerSubscriptions()`, which returns the function that unregisters them.

```ts
// Routes events to the registered paginators from now on.
const unregister = client.channelManager.registerSubscriptions();

// When the app no longer needs live channel lists, for example when the user logs out.
// Registrations are counted, so the manager stops listening only once every one is
// released, including those of mounted `ChannelList` components.
unregister();
```

## How events move channels between paginators

When an event changes a channel, the manager routes it. The channel joins every registered paginator whose filters it matches, at its sort position, and leaves the others.

| Events                                                                                                                            | What the manager does                                                                                                                                                              |
| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message.new`, `notification.message_new`, `notification.added_to_channel`, `channel.visible`, `channel.hidden`, `member.updated` | Routes the channel. A channel the client doesn't have yet is loaded first (see the next section).                                                                                  |
| `channel.updated`, `channel.truncated`, `message.deleted` with `hard_delete`                                                      | Routes the channel if the client has it. A changed field, such as the name or the latest message, can move or drop it.                                                             |
| `channel.deleted`, `notification.removed_from_channel`                                                                            | Removes the channel from every paginator.                                                                                                                                          |
| `message.read`, `message.read_locally`, `notification.mark_read`, `notification.mark_unread`                                      | Routes the channel for the current user's own reads of the channel, not of a thread, and only when a paginator filters on `has_unread` or sorts by `has_unread` or `unread_count`. |
| `notification.channel_mutes_updated`                                                                                              | Routes every loaded channel, so paginators filtered on `muted` follow the change.                                                                                                  |

>
> **Info:** **Example**
>
> The user archives "Design" with `Channel.archive()`. Nothing moves yet. The server sends `member.updated`, which sets their membership's `archived_at`. The manager routes "Design". It no longer matches the inbox, `{ archived: false }`, and it matches the archive, `{ archived: true }`, so it leaves the inbox and joins the archive.
>

The client matches these channel fields:

- `archived` and `pinned` from the user's membership, `muted` from their mute list, and `has_unread` from their read state;
- `members` from the loaded members, the current user always included, and `member.user.name` from the loaded members;
- `hidden`, `type`, `id`, `cid`, `last_updated`, `app_banned`, and any field of the channel's data, custom fields included.

A hidden channel never matches a paginator whose filters don't mention `hidden`, as on the server.

## Channels the client hasn't seen

>
> **Info:** **Example**
>
> Bob starts a direct conversation with the user. The user's client has never loaded that channel, so `notification.added_to_channel` names a channel it doesn't have. The channel manager gets the channel, watches it, then routes it, and the conversation appears in the user's inbox.
>

It does this for every event in the first row of the table above. To stop it, so that the paginators hold only channels they loaded themselves, add the `ignoreEventsForUnknownChannels` handler at the start of each of those events' pipelines. For a channel the client doesn't have, it ends the event's handling there, so the manager doesn't get or watch the channel and doesn't add it to any paginator:

```ts
import { ignoreEventsForUnknownChannels } from "stream-chat";
import type { StreamChat } from "stream-chat";

export const ignoreUnknownChannels = (client: StreamChat) =>
  (
    [
      "channel.hidden",
      "channel.visible",
      "member.updated",
      "message.new",
      "notification.added_to_channel",
      "notification.message_new",
    ] as const
  ).map((eventType) =>
    client.channelManager.addEventHandler({
      eventType,
      handle: ignoreEventsForUnknownChannels,
      id: `app:ignore-unknown-channels:${eventType}`,
      index: 0,
    }),
  );
```

With this in place, a channel the user was just added to doesn't appear until a paginator loads it, for example on its next `ChannelPaginator.reload()`.

## Customizing event handling

The manager keeps one **pipeline** per event type, an ordered list of handlers. Each event runs through the handlers of its type, in order:

- a handler receives `{ event, ctx }`, where `ctx.channelManager` is the manager;
- returning `{ action: "stop" }` ends the run, so later handlers don't see the event;
- a handler can be async, and events of one type are handled one at a time, in the order they arrived;
- a handler that throws is logged, and the next handler still runs.

The client applies an event to the channel's state before the manager sees it, so a handler reads the updated channel.

The built-in handlers have the ids `ChannelManager:default-handler:<event type>`, for example `ChannelManager:default-handler:message.new`. The four read events share `ChannelManager:default-handler:read-state-changed`. `ChannelManager.getDefaultHandlers()` returns a copy of all of them.

This handler boosts a channel in every paginator for a minute when a new message mentions the current user. It's inserted before the built-in handler, so the channel is placed with its boost:

```ts
const unsubscribe = client.channelManager.addEventHandler({
  eventType: "message.new",
  handle: ({ ctx: { channelManager }, event }) => {
    const { cid, message } = event;
    const mentionsMe = message?.mentioned_users?.some(
      ({ id }) => id === client.userId,
    );
    if (!cid || !mentionsMe) return;
    channelManager.paginators.forEach((paginator) =>
      paginator.boost(cid, { ttlMs: 60_000 }),
    );
  },
  id: "app:boost-mentions",
  index: 0,
});
```

The other methods change a pipeline as a whole:

- **`ChannelManager.addEventHandler({ eventType, handle, id, index, replace, revertOnUnsubscribe })`** inserts a handler at `index`, or at the end, and returns the function that removes it. With `replace: true` it replaces the handler at `index`, and `revertOnUnsubscribe: true` puts that one back when you remove yours.
- **`ChannelManager.setEventHandlers({ eventType, handlers })`** replaces every handler of the event type.
- **`ChannelManager.removeEventHandlers({ eventType, handlers })`** removes the handlers that match. Each entry in `handlers` matches by the function, `{ handler }`, or by the id, `{ idMatch: { id } }`, where `id` can be a `RegExp` to match several.

For example, this removes the mention booster added above:

```ts
client.channelManager.removeEventHandlers({
  eventType: "message.new",
  handlers: [{ idMatch: { id: "app:boost-mentions" } }],
});
```

Any event type can have a pipeline, custom events included.

## Channels that match several paginators

By default, a channel is in every paginator whose filters it matches. The user's pinned paginator, `{ pinned: true }`, and their inbox, `{ archived: false }`, both match a pinned channel, so it's in both.

To keep each channel in one paginator, set an ownership rule. With an array of paginator ids, highest priority first, the matching paginator that ranks highest keeps the channel:

```ts
import { ChannelPaginator } from "stream-chat";
import type { StreamChat } from "stream-chat";

export const registerPinnedAndInbox = (client: StreamChat, userId: string) => {
  const pinned = new ChannelPaginator({
    client,
    filters: { members: { $in: [userId] }, pinned: true },
    id: "pinned",
  });
  const inbox = new ChannelPaginator({
    client,
    filters: { archived: false, members: { $in: [userId] } },
    id: "inbox",
  });

  client.channelManager.setPaginators([pinned, inbox]);
  // A pinned channel is in "pinned" only.
  client.channelManager.setOwnershipResolver(["pinned", "inbox"]);

  return { inbox, pinned };
};
```

A function works too. It receives `{ channel, matchingPaginators }` and returns the ids of the paginators that keep the channel. An empty array keeps it in every matching paginator. This one keeps channels with two members in "inbox" only:

```ts
import type { StreamChat } from "stream-chat";

export const keepTwoMemberChannelsInInbox = (client: StreamChat) =>
  client.channelManager.setOwnershipResolver(
    ({ channel, matchingPaginators }) =>
      channel.data?.member_count === 2
        ? matchingPaginators
            .filter(({ id }) => id === "inbox")
            .map(({ id }) => id)
        : [],
  );
```

- A paginator missing from the array ranks lowest. When none of the matching paginators is in the array, the channel stays in all of them.
- `ChannelManager.setOwnershipResolver()` without an argument goes back to the default.

The rule applies to events and to the pages each paginator loads itself. A page from the server leaves out the channels another paginator owns.

A channel that matches no paginator isn't in any. To catch those, register a paginator that matches every channel of the user, such as `{ members: { $in: [userId] } }`, with the lowest priority.

## Changing filters or sort

A paginator's filters and sort can change. Set `paginator.staticFilters` or `paginator.sort`, or change the context of `paginator.filterBuilder`. The paginator then sends and matches the new values, but it keeps the channels it loaded with the old ones. Call `ChannelPaginator.reload()` after the change, so the paginator starts again from its first page:

```ts
paginator.staticFilters = { ...paginator.staticFilters, team: "design" };
paginator.sort = [{ direction: -1, field: "updated_at" }];
await paginator.reload();
```

Without `ChannelPaginator.reload()`, the next load keeps the paginator's old position. It starts at the offset the paginator had reached, so it skips the channels before it, and it loads nothing when the paginator had no more channels to load.

## Used in

- [Channel lists](https://getstream.io/chat/docs/sdk/react-native/v10/features/channels/channel-lists/)
- [Quick start](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/quick-start/)

---

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