# Channel list paginators

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

A `ChannelPaginator` holds the channels of a channel list: 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. 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/v15/state/loading-data/how-pagination-works/), which explains loading, staying live and ordering for every paginator.

## Creating a channel paginator

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

const inboxFilters: ChannelFilters = {
  archived: false,
  members: { $in: [userId] },
  type: "messaging",
};
const latestFirst: SortParamRequest[] = [
  { direction: -1, field: "last_message_at" },
];

const inbox = 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`.

## 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";

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";

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

// Later, when the archive is no longer shown. It stops getting event updates and keeps the
// channels it loaded, so it can be registered again.
client.channelManager.removePaginator("archive");
```

To render a list for each registered paginator, subscribe to `client.channelManager.state` and select `paginators`. It holds them in their registration order, and publishes when one is registered, moved or removed. `client.channelManager.paginators` reads the current set once.

Registering a paginator loads nothing. The paginator loads its first page when something calls `ChannelPaginator.toTail()` or `ChannelPaginator.toTailDebounced()`. `ChannelList` calls `ChannelPaginator.toTailDebounced()` whenever its paginator has nothing loaded, including after a reload.

The manager handles events once its subscriptions are registered: the React SDK does that for you. Code that uses `stream-chat` alone 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: the manager stops listening once every one is released.
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 }`. 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: the manager doesn't get or watch the channel, and doesn't add it to any paginator.

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

const unsubscribers = (
  [
    "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 logged in 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";

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"]);

// A function works too: ({ channel, matchingPaginators }) => string[] returns the ids of the
// paginators that keep the channel. An empty array keeps it in every matching paginator.
// This one keeps direct messages in "inbox" only.
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 mixes channels that match the new filters into the channels it already holds, and can skip channels.

## Used in

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

---

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