# How pagination works

> **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 paginator is a state layer object that loads items page by page and keeps them current as events arrive. Every paginator in `stream-chat` works the same way:

| Paginator                         | Holds                            | Created by  | Details                                                                                          |
| --------------------------------- | -------------------------------- | ----------- | ------------------------------------------------------------------------------------------------ |
| `ChannelPaginator`                | the channels of one channel list | your code   | [Channel list paginators](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/channel-list-paginators/)  |
| `channel.messagePaginator`        | a channel's messages             | the channel | [Message paginators](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/message-paginators/)            |
| `thread.messagePaginator`         | a thread's replies               | the thread  | [Message paginators](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/message-paginators/)            |
| `channel.pinnedMessagesPaginator` | a channel's pinned messages      | the channel | [Pinned messages paginator](https://getstream.io/chat/docs/sdk/react/v15/reference/state/pinned-messages-paginator/) |
| `client.threads.paginator`        | the user's threads               | the client  | [Thread and thread manager](https://getstream.io/chat/docs/sdk/react/v15/reference/state/thread-and-thread-manager/) |
| `client.reminders.paginator`      | the user's reminders             | the client  | [Reminder manager](https://getstream.io/chat/docs/sdk/react/v15/reference/state/reminder-manager/)                   |

This page explains what they have in common. Their methods and options are listed in [Channel manager and ChannelPaginator](https://getstream.io/chat/docs/sdk/react/v15/reference/state/channel-manager-and-channelpaginator/) and [Message paginator](https://getstream.io/chat/docs/sdk/react/v15/reference/state/message-paginator/).

## Defined by filters and sort

>
> **Info:** **Example**
>
> A paginator of the user's messaging channels, the most recently active first, has two parts:
>
> - the **filters**, `{ members: { $in: [userId] }, type: "messaging" }`, decide which channels belong to the paginator;
> - the **sort**, `[{ field: "last_message_at", direction: -1 }]`, decides their order.
>

If either changes, the items already loaded no longer match the paginator. The paginator has to load again from the beginning: the next load is a first page, and the old items are discarded. For a `ChannelPaginator` you do this with `ChannelPaginator.reload()`, as [Changing filters or sort](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/channel-list-paginators/#changing-filters-or-sort) explains.

The message paginators, for a channel's messages, a thread's replies and the pinned messages, have a fixed definition. Their channel or thread sets it, and you can't change it.

Which paginators take filters and sort:

| Paginator                                             | Filters and sort                                                                                                                                                                   |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ChannelPaginator`                                    | Yours: `filters` and `sort` when you create it, or later through `staticFilters`, `filterBuilder` and `sort`. A change doesn't reload by itself: call `ChannelPaginator.reload()`. |
| `client.reminders.paginator`                          | Yours: set `filters` or `sort` on it. The default sort is `created_at`, oldest first. Setting either one clears the loaded reminders, so the next load starts from the first page. |
| `client.threads.paginator`                            | None. The server decides the order: unread threads first, then by latest reply.                                                                                                    |
| `channel.messagePaginator`, `thread.messagePaginator` | Fixed: the channel's or thread's messages, by `created_at`.                                                                                                                        |
| `channel.pinnedMessagesPaginator`                     | Fixed: the channel's pinned messages, by `pinned_at`.                                                                                                                              |

## Intervals

A paginator keeps what it has loaded as **intervals**. An interval is a collection of pages of items retrieved from the server during pagination, with nothing missing between them. Usually there is one: the first page and every page loaded after it join it.

A second interval appears when a message paginator jumps to a message that isn't loaded, far from what is. The page around that message can't join the loaded messages, because the ones in between are missing, so it becomes an interval of its own. When later loads close the gap, the two intervals join.

`items` in the paginator's `state` holds one interval, the one on screen. [Message paginators](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/message-paginators/#intervals-after-a-jump) shows how jumps move between intervals.

## Head and tail

Every paginator has two ends:

- the **head** is the end the first page loads from;
- the **tail** is the other end. Later pages load toward it.

Which item is at the head depends on the paginator's sort.

A `ChannelPaginator` sorted by latest activity has the most recently active channel at its head, the top of the channel list. A channel that gets a new message sorts first, so it moves there, and scrolling down loads more channels toward the tail.

A message paginator loads the newest messages first.

The reminder paginator sorts by `created_at`, oldest first by default, so its head is the oldest reminder.

## Loading more

Every paginator loads more the same way: `paginator.toTail()` loads the next page toward the tail, and `paginator.toHead()` toward the head. A paginator loads its first page from its head, so usually only the tail has more.

Every paginator also keeps its data in a `state` store with the same fields:

| Field            | What it holds                                                                                                                                                                                                                      |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `items`          | The loaded items of the interval on screen. `undefined` until a page loads.                                                                                                                                                        |
| `isLoading`      | Whether a page is loading.                                                                                                                                                                                                         |
| `hasMoreTail`    | Whether there are more items toward the tail, beyond the interval in `items`.                                                                                                                                                      |
| `hasMoreHead`    | Whether there are more items toward the head, beyond the interval in `items`. Usually `false`, but `true` after a message paginator jumps to an older interval, until `MessagePaginator.toHead()` loads up to the newest messages. |
| `lastQueryError` | The error of the last failed load. A successful load clears it.                                                                                                                                                                    |

This paginator loads the user's messaging channels and logs every change:

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

const inbox = new ChannelPaginator({
  client,
  filters: { members: { $in: [userId] }, type: "messaging" },
  sort: [{ direction: -1, field: "last_message_at" }],
});

// The handler runs once right away, then whenever one of the selected fields changes.
const unsubscribe = inbox.state.subscribeWithSelector(
  ({ hasMoreTail, isLoading, items, lastQueryError }) => ({
    hasMoreTail,
    isLoading,
    items,
    lastQueryError,
  }),
  ({ hasMoreTail, isLoading, items, lastQueryError }) => {
    if (lastQueryError) console.error("Couldn't load channels", lastQueryError);
    console.log(`${items?.length ?? 0} channels`, { hasMoreTail, isLoading });
  },
);

await inbox.toTail(); // the first page
await inbox.toTail(); // the next page
```

A few rules apply to every load:

- **One load at a time.** While a page is loading, another `paginator.toTail()`, `paginator.toHead()` or `paginator.reload()` returns at once without loading anything, and isn't retried later. So does a load toward an end that has no more items.
- **Debounced loads for scrolling.** The rule above only stops calls while a request is in flight. Once a page arrives, the next scroll event at the edge would load another page right away, and a scroll handler fires many times a second. `paginator.toTailDebounced()` and `paginator.toHeadDebounced()` wait for calls to stop for `debounceMs` (300 ms by default), then load once, so a scroll handler can call them on every scroll event.
- **Retries.** A failed request is retried `retryCount` times, one second apart, and `lastQueryError` is set only once the last attempt fails. The default is 0, and a `ChannelPaginator` retries 3 times.
- **Errors.** A failed load, whether the request fails or placing its page does, sets `lastQueryError` and doesn't throw. Pass `throwErrors: true` in the paginator options to make `paginator.toTail()`, `paginator.toHead()` and `paginator.reload()` reject instead.
- **Reload.** `paginator.reload()` clears the paginator and loads its first page again. Until that page arrives, `items` is `undefined` and `isLoading` is `true`, so a list rendered from it shows as loading.

The debounce delay, the retries and whether a failed load rejects are configurable per paginator type, with `debounceMs`, `retryCount` and `throwErrors`.

After a reconnect, the client refreshes each `ChannelPaginator` that has loaded, without clearing it: `ChannelPaginator.recoverLoadedItems()` queries again, page by page, the channels it has loaded, up to 150. They get their latest data and sort position, channels that now match join, and the list stays on screen meanwhile. Then the loaded channels no page returned are removed, and those beyond the first 150 are dropped until the list is scrolled to them again. [What reloads after a reconnect](https://getstream.io/chat/docs/sdk/react/v15/state/connection/#what-reloads-after-a-reconnect) explains when that happens.

## Staying live

A paginator doesn't query the server again for every event. When an event changes an item, the client checks the item against the paginator's own filters, and the item joins the paginator, moves, or leaves it.

>
> **Info:** **Example**
>
> The user has an inbox paginator with `{ archived: false }` and an archive paginator with `{ archived: true }`. They archive "Design". The server answers with a `member.updated` event that sets their membership's `archived_at`. "Design" no longer matches the inbox, so it leaves the inbox and joins the archive at its sort position.
>

The client supports these filter operators: `$eq`, `$ne`, `$in`, `$nin`, `$gt`, `$gte`, `$lt`, `$lte`, `$exists`, `$contains` and `$autocomplete`, combined with `$and`, `$or` and `$nor`.

Avoid other operators in a paginator's filters. The server still applies them when it loads pages, so the first pages look right. But when an event changes a loaded item, the client checks the item against the filters itself, and a filter it can't evaluate always fails. For example, when a channel in the list gets a new message, it disappears from the list, even though it still matches.

An item that sorts after the last loaded item can't be placed exactly, because unloaded items may come before it. If it's already in the paginator, it stays after the last loaded item until the next page places it. If it newly matches the filters, it isn't added to `items` until a page reaches its position.

## Ordering and boosting

Items follow the paginator's sort. A `ChannelPaginator` usually sorts by activity, such as `last_message_at`, so channels change places as messages arrive. Two settings control that:

- **`lockItemOrder`.** With `true`, a channel already in `items` keeps its position when it's updated, instead of moving to its new sort position. Use it when rows moving under the user's pointer are worse than a briefly stale order.
- **Boosting.** `paginator.boost(cid, { ttlMs })` puts a channel first in `items` until the boost expires, 15 seconds by default. It only reorders the loaded channels: boosting a channel that isn't loaded doesn't add it. The boost takes effect the next time the paginator publishes its items, and has no effect with `lockItemOrder`. The client never boosts on its own.

This paginator keeps its order while the user reads it:

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

const inbox = new ChannelPaginator({
  client,
  filters: { members: { $in: [userId] } },
  paginatorOptions: { lockItemOrder: true },
});
```

This function keeps a channel the user just opened first in a paginator for a minute, if it's among the channels the paginator has loaded:

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

const keepOnTop = (paginator: ChannelPaginator, opened: Channel) => {
  paginator.boost(opened.cid, { ttlMs: 60_000 });
  // Places the channel again, so the boost shows now instead of on its next event.
  client.channelManager.ingestChannel(opened);
};
```

## Local and optimistic items

A paginator can hold items the server hasn't confirmed yet. When the user sends "Hello", the message joins the channel's message paginator at once with status `sending`. When the server answers, its copy replaces the local one and the status becomes `received`. If the request fails, the message stays in the paginator as `failed`, ready to be retried.

Items are shared between paginators. A message is one object for every paginator that holds it, so a change made anywhere shows everywhere at once. For example, the user reacts to the parent message of a thread: the reaction shows in the channel's message paginator and in the thread's.

When the server answers, its copy replaces the local one, unless a newer copy arrived in the meantime, for example in an event.

A `ChannelPaginator` has no optimistic changes. A channel moves when the server's event arrives, not when the call is made: archiving moves it on `member.updated`. [What's optimistic and what isn't](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/whats-optimistic-and-what-isnt/) lists every action and how it settles.

## Paginators vs one-off queries

### One-shot queries

These methods return plain data, once. The data isn't added to any paginator, isn't cached, and nothing updates it:

- channels and threads: `client.queryChannels()`, `client.queryThreads()`;
- users and members: `client.queryUsers()`, `Channel.queryMembers()`;
- messages: `client.getMessage()`, `client.getReplies()`, `Channel.getPinnedMessages()`, `client.search()`;
- what belongs to messages: `client.queryReactions()`, `client.queryReminders()`, `client.queryDrafts()`.

The result is a snapshot. A new member doesn't join the result of `Channel.queryMembers()`, and a deleted message doesn't leave the result of `client.search()`. To refresh a result, run the query again.

```ts
// A snapshot: which of these users exist right now.
const { users } = await client.queryUsers({
  payload: { filter_conditions: { id: { $in: ["ann", "bob"] } } },
});
```

`client.queryChannelsAndHydrate()`, `client.queryThreadsAndHydrate()` and `client.getThreadAndHydrate()` return `Channel` and `Thread` objects instead. They add nothing to a paginator either, and the list they return doesn't change. The channels are the client's own, kept in its store and updated by events. A thread is the client's own only when the client already held it; see [Shared entities](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/shared-entities/#what-not-to-do).

### Paginators

Paginators stay current as events arrive, load in both directions, and a message paginator can jump to any message. They also share their items with the rest of the state layer, so a message edited in one paginator changes in every other.

### Which to use

- To render something and keep it current, use a paginator: a `ChannelPaginator`, a channel's or a thread's `messagePaginator`, `channel.pinnedMessagesPaginator`, or `client.threads.paginator`.
- For a lookup or a snapshot, such as checking whether a user exists or exporting a channel's members, use a query.
- For search results, use [Search sources](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/search-sources/). They load page by page like a paginator, but don't stay current.

## Used in

- [Channel lists](https://getstream.io/chat/docs/sdk/react/v15/features/channels/channel-lists/)
- [Replies and threads](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/replies-and-threads/)
- [Pinned messages](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/pinned-messages/)
- [Reminders and save for later](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/reminders-and-save-for-later/)

---

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