# How pagination works

> **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 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-native/v10/state/loading-data/channel-list-paginators/)  |
| `channel.messagePaginator`        | a channel's messages             | the channel | [Message paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/)            |
| `thread.messagePaginator`         | a thread's replies               | the thread  | [Message paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/)            |
| `channel.pinnedMessagesPaginator` | a channel's pinned messages      | the channel | [Pinned messages paginator](https://getstream.io/chat/docs/sdk/react-native/v10/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-native/v10/reference/state/thread-and-thread-manager/) |
| `client.reminders.paginator`      | the user's reminders             | the client  | [Reminder manager](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/reminder-manager/)                   |

You create a `ChannelPaginator` and pass it to `ChannelList`. The other paginators exist as soon as their channel, thread or client does.

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-native/v10/reference/state/channel-manager-and-channelpaginator/) and [Message paginator](https://getstream.io/chat/docs/sdk/react-native/v10/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-native/v10/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. Pass `filters` and `sort` when you create it, or change them later through `staticFilters`, `filterBuilder` and `sort`. A change doesn't reload by itself, so call `ChannelPaginator.reload()`.                                                                                                         |
| `client.reminders.paginator`                          | Yours. Set `filters` or `sort` on it. Without a `sort`, the request sends none, so the server decides which reminders the first page holds, and the paginator orders what it loads by `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, and 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-native/v10/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. Its head is the newest message, at the bottom of the message list, and scrolling up loads older messages toward its tail.

The reminder paginator orders its reminders by `created_at`, oldest first, unless you set a `sort`, so its head is the oldest reminder it has loaded.

## 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.

The SDK's components call these for you. `ChannelList` loads the first page whenever its paginator has nothing loaded, and the next one when the user scrolls near the end. The message lists load older messages when the user scrolls near the top, and newer ones near the bottom while `hasMoreHead` is `true`.

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";
import type { StreamChat } from "stream-chat";

export const loadInbox = async (client: StreamChat, userId: string) => {
  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

  return { inbox, unsubscribe };
};
```

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. With [offline support](https://getstream.io/chat/docs/sdk/react-native/v10/state/offline-support/offline-support/), a failed request of a channel list that already shows channels keeps them and sets no error.
- **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 thread list, `client.threads.paginator`, is the exception. Its `ThreadPaginator.reload()` loads the first page even while another load is in flight, and keeps the threads on screen until the new page arrives. Its `ThreadPaginator.toTail()` does nothing before the first `ThreadPaginator.reload()`.

`debounceMs`, `retryCount` and `throwErrors` set the debounce delay, the retries and whether a failed load rejects. A `ChannelPaginator` takes them in `paginatorOptions`, and the message paginators and `channel.pinnedMessagesPaginator` take them from `client.config`, as [Configuration](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/channel-manager-and-channelpaginator/#configuration) explains.

After a reconnect, the client refreshes each `ChannelPaginator` registered on `client.channelManager` that has loaded, without clearing it. It queries every page the list has loaded again, starting from the first, up to 150 channels, and the list stays on screen meanwhile. The loaded channels get their latest data and sort position, and channels that now match join. Loaded channels that no page returned leave the list: they were deleted, hidden or left, or they no longer match the filters. Channels loaded past the first 150 are dropped too, and load again when the user scrolls to them. If a page fails, the refresh stops and removes nothing. [What reloads after a reconnect](https://getstream.io/chat/docs/sdk/react-native/v10/state/connection/#what-reloads-after-a-reconnect) explains when that happens. To run this refresh on one list from your own code, call `ChannelPaginator.recoverLoadedItems()`.

Pulling down on `ChannelList` refreshes less: it queries the first page again and merges it into the channels the list has, without removing any. To do that from your own code, call `ChannelPaginator.toTail({ keepPreviousItems: true, reset: "yes" })`.

With offline support, a channel list's first page shows the channels the local database holds for its query at once. The request to the server waits until the client has synced the events it missed, and its page then replaces those channels.

## Staying live

A paginator doesn't query the server again for every event. A `ChannelPaginator` follows events only once it's registered on `client.channelManager` with `ChannelManager.insertPaginator()`, and while the manager's subscriptions are on, which a mounted `ChannelList` does for you (see [Channel list paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/channel-list-paginators/)). The samples on this page aren't registered, so they load pages but don't follow events. 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 finger 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, so 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";
import type { StreamChat } from "stream-chat";

export const createStableInbox = (client: StreamChat, userId: string) =>
  new ChannelPaginator({
    client,
    filters: { members: { $in: [userId] } },
    paginatorOptions: { lockItemOrder: true },
  });
```

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

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

export const keepOnTop = (
  client: StreamChat,
  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, and 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, for example, moves it on `member.updated`. [What's optimistic and what isn't](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/whats-optimistic-and-what-isnt/) lists every action and how it settles.

## Paginators vs one-off queries

### One-off queries

These methods return plain data, once. The data isn't added to any paginator, and nothing updates it. Two of them have side effects: `client.queryChannels()` watches the channels it returns unless you pass `watch: false`, and `client.queryUsers()` stores the users it returns in `client.state`.

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

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 channel list or to the thread list 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-native/v10/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-native/v10/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-native/v10/features/channels/channel-lists/)
- [Replies and threads](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/replies-and-threads/)
- [Pinned messages](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/pinned-messages/)
- [Reminders and save for later](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/reminders-and-save-for-later/)

---

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