# Message paginator

> **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 `MessagePaginator` is a message list. Every channel has one in `channel.messagePaginator`, and every thread has one for its replies in `thread.messagePaginator`. Inside a thread, read the thread's list, not its channel's. This page lists its API in `stream-chat`.

A message paginator has everything every paginator has, listed in [The base paginator](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/channel-manager-and-channelpaginator/#the-base-paginator). What only message paginators do, such as intervals, jumping and the unread snapshot, is explained on [Message paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/).

## Requesting vs. rendering order, jumping, and unread state

A message list keeps its data in five stores, each for one concern, so a subscriber wakes only for the changes it reads:

| Store                 | What it holds                                                                 |
| --------------------- | ----------------------------------------------------------------------------- |
| `state`               | The messages of the interval on screen, and the loading state.                |
| `messageFocusSignal`  | The message to highlight after a jump.                                        |
| `unreadStateSnapshot` | Where the user's unread messages started when the list loaded.                |
| `liveViewState`       | Whether the user is viewing the newest messages.                              |
| `aggregateState`      | The newest message of the channel or thread, whichever interval is on screen. |

The SDK creates a list with its channel or thread. Apps don't construct one. The two kinds differ in how they request pages:

|               | Channel list                                  | Thread replies                               |
| ------------- | --------------------------------------------- | -------------------------------------------- |
| Request       | The channel query, with `messages` pagination | `client.getReplies()` for the parent message |
| `requestSort` | `created_at` ascending, not sent              | `created_at` descending: newest page first   |
| `itemOrder`   | `created_at` ascending                        | `created_at` ascending                       |
| Page size     | 100, or 25 once `Channel` has mounted         | 50                                           |

`requestSort` is the order pages are requested in, and `itemOrder` is the order of `items`. In both lists, `items` is oldest first. The `Channel` component sets a channel's page size to 25 when no page size is registered in `client.config`. See [Loading more](#loading-more).

These properties identify a list:

| Property          | What it holds                                                  |
| ----------------- | -------------------------------------------------------------- |
| `channel`         | The channel. A thread's list holds the thread's channel.       |
| `parentMessageId` | The parent message's id for a thread's list, else `undefined`. |
| `id`              | The list's id.                                                 |
| `requestSort`     | The sort pages are requested with.                             |
| `itemOrder`       | The order of `items`.                                          |

## Reading the message list

`state` has the base paginator's fields:

| Field            | What it holds                                                                                 |
| ---------------- | --------------------------------------------------------------------------------------------- |
| `items`          | The messages of the interval on screen, oldest first. `undefined` until the first page loads. |
| `isLoading`      | A page is loading.                                                                            |
| `hasMoreTail`    | There are older messages to load.                                                             |
| `hasMoreHead`    | There are newer messages to load: the interval on screen isn't the newest one.                |
| `lastQueryError` | The error of the last failed load.                                                            |
| `cursor`         | The ids the next pages start from.                                                            |

Each message is a `LocalMessage`: its `status` is `"sending"`, `"received"` or `"failed"`, and its timestamps are in nanoseconds.

This handler runs whenever the messages on screen or the loading state change:

```ts
const { messagePaginator } = channel;

const unsubscribe = messagePaginator.state.subscribeWithSelector(
  ({ hasMoreTail, isLoading, items }) => ({ hasMoreTail, isLoading, items }),
  ({ hasMoreTail, isLoading, items }) => {
    console.log(`${items?.length ?? 0} messages`, { hasMoreTail, isLoading });
  },
);
```

Two methods look up loaded messages:

| Method                                         | Returns                       | What it does                                                                                                                                                                                |
| ---------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getItem(id)`                                  | `LocalMessage` or `undefined` | A loaded message, from any interval.                                                                                                                                                        |
| `findItemByTimestamp(timestamp, exactTsMatch)` | `LocalMessage` or `null`      | The last loaded message created at or before `timestamp`, in nanoseconds, from any interval, newest interval first. With `exactTsMatch` set to `true`, only a message created at that time. |

## Loading more

| Method                           | What it does                                                               |
| -------------------------------- | -------------------------------------------------------------------------- |
| `toTail(params)`                 | Loads older messages.                                                      |
| `toHead(params)`                 | Loads newer messages, after a jump.                                        |
| `toTailDebounced(params)`        | `toTail()` once calls stop, for a scroll handler.                          |
| `toHeadDebounced(params)`        | `toHead()` once calls stop.                                                |
| `setPruningSuspended(suspended)` | Pauses the memory cap while the user reads near the oldest loaded message. |

`params` is described on [The base paginator](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/channel-manager-and-channelpaginator/#loading). `MessagePaginator.toTail({ requestOptions: { watch: true } })` on a channel's list also watches the channel with the same request.

`MessageList` and `MessageFlashList` call `MessagePaginator.toTail()` when the user scrolls near the oldest loaded message and `MessagePaginator.toHead()` near the newest one after a jump. With `maxLoadedItems` set, they also call `MessagePaginator.setPruningSuspended()` as the user scrolls near the oldest loaded messages, and lift it when they unmount.

The SDK loads a channel's first page itself: `Channel.watch()` and channel list pages bring the newest messages. A thread's list starts with the replies its thread came with, or loads them when the thread is opened, as [Thread wiring](#thread-wiring-and-minimal-initialization) explains. `Channel.reload()` and `Thread.reload()` refresh the newest loaded messages in place. A reader on an older interval stays there.

Message lists take these settings from `client.config`. The `messagePaginator` key applies to channels and threads, and the `messagePaginator` inside `channel` or `thread` overrides it for one of them:

| Setting                                                    | Default                            | What it does                                                                                                                         |
| ---------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `pageSize`                                                 | 100 for a channel, 50 for a thread | How many messages a page asks for. Without a registered value, `Channel` sets a channel's to 25 when it mounts.                      |
| `stateThrottleMs`                                          | 500                                | Publishes live changes to `items` at most once per interval. The user's own changes publish at once.                                 |
| `maxLoadedItems`                                           | none                               | Caps the newest interval. Once it holds more, the oldest are dropped and `hasMoreTail` becomes `true`.                               |
| `unreadReferencePolicy`                                    | `"snapshot"`                       | What `MessagePaginator.jumpToTheFirstUnreadMessage()` reads first. See [Unread state](#unread-state). Read when the list is created. |
| `debounceMs`, `retryCount`, `throwErrors`, `lockItemOrder` | as for every list                  | See [Configuration](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/channel-manager-and-channelpaginator/#configuration).            |

```ts
client.config.set({
  // every message paginator
  messagePaginator: { stateThrottleMs: 300 },
  // channels only
  channel: { messagePaginator: { maxLoadedItems: 500, pageSize: 50 } },
  // threads only
  thread: { messagePaginator: { pageSize: 30 } },
});
```

Register these before any `Channel` mounts. `Channel` sets the page size to 25 only when no page size is registered, and a later change under the `channel`, `messagePaginator` or `messageOperations` key derives the list's configuration again, which replaces the 25.

[Throttling and the memory cap](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/#throttling-and-the-memory-cap) explains both.

## Jumping to a message

| Method                                 | Returns            | What it does                                                                                                               |
| -------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `jumpToMessage(messageId, options)`    | `Promise<boolean>` | Shows the interval that holds the message, loading a page around it if it isn't loaded.                                    |
| `jumpToTheLatestMessage(options)`      | `Promise<boolean>` | Shows the newest interval, loading it if needed. Doesn't highlight anything, unless you pass `suppressFocusSignal: false`. |
| `jumpToTheFirstUnreadMessage(options)` | `Promise<boolean>` | Shows the first message the user hasn't read. See [Unread state](#unread-state).                                           |

Each resolves to `true` when it got there and `false` when it didn't. A failed `MessagePaginator.jumpToMessage()` or `MessagePaginator.jumpToTheLatestMessage()` also publishes an error notification through `client.notifications`. A jump that a later jump or reload overtakes resolves to `false` without a notification. Its page stays loaded but isn't shown.

`options` is optional and takes these fields:

| Field                 | What it does                                                                                                |
| --------------------- | ----------------------------------------------------------------------------------------------------------- |
| `pageSize`            | How many messages to load around the target. Defaults to the page size.                                     |
| `focusReason`         | The `reason` the focus signal carries: `"jump-to-message"`, `"jump-to-first-unread"` or `"jump-to-latest"`. |
| `focusSignalTtlMs`    | How long the highlight lasts once the message is on screen. 3000 ms by default.                             |
| `suppressFocusSignal` | `true` jumps without a focus signal.                                                                        |
| `watchChannel`        | `true` also watches the channel with the same request, if it isn't watched. Ignored for a thread's replies. |

After a jump, `messageFocusSignal` holds `{ signal }`. `signal` is `{ messageId, reason, token, createdAt, ttlMs }`, or `null`. The `token` changes on every jump, so two jumps to the same message give two signals. The signal stays until it's cleared:

| Method                                                 | What it does                                                                                         |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `scheduleMessageFocusSignalClear({ token, ttlMs })`    | Clears the signal after `ttlMs`, or the signal's own `ttlMs`. Call it once the message is on screen. |
| `clearMessageFocusSignal({ token })`                   | Clears the signal at once.                                                                           |
| `emitMessageFocusSignal({ messageId, reason, ttlMs })` | Publishes a signal without jumping, and returns it.                                                  |

With a `token`, the first two act only while that signal is the current one.

`MessageList` and `MessageFlashList` follow the signal: they scroll the message to the middle of the list, highlight it, and call `MessagePaginator.scheduleMessageFocusSignalClear()` once it's on screen. A message list of your own does the same:

```ts
const { messagePaginator } = channel;

const unsubscribe = messagePaginator.messageFocusSignal.subscribeWithSelector(
  ({ signal }) => ({ signal }),
  ({ signal }) => {
    if (!signal) return;
    // Scroll your list to signal.messageId, then start the countdown.
    console.log("Scroll to", signal.messageId);
    messagePaginator.scheduleMessageFocusSignalClear({ token: signal.token });
  },
);

await messagePaginator.jumpToMessage(messageId);
```

## Unread state

`unreadStateSnapshot` keeps a copy of the user's unread state, taken when the list loads its first page. Marking the channel read doesn't change it, so the UI can still show where the user left off.

| Field                  | What it holds                                                   |
| ---------------------- | --------------------------------------------------------------- |
| `lastReadAt`           | When the user last read the channel, in nanoseconds, or `null`. |
| `lastReadMessageId`    | The last message the user read, or `null`.                      |
| `firstUnreadMessageId` | The message the user marked unread from, or `null`.             |
| `unreadCount`          | How many messages were unread.                                  |

| Method                     | What it does                                                                                       |
| -------------------------- | -------------------------------------------------------------------------------------------------- |
| `setUnreadSnapshot(patch)` | Changes some fields, and returns the new snapshot.                                                 |
| `seedUnreadSnapshot()`     | Takes the snapshot again from the channel's current read state, with `firstUnreadMessageId: null`. |
| `clearUnreadSnapshot()`    | Resets every field. Call it once the user has caught up.                                           |

The channel also changes the snapshot: a new message that counts as unread raises `unreadCount`, `notification.mark_unread` sets every field, and truncating the channel clears it. `Channel` calls `MessagePaginator.seedUnreadSnapshot()` each time it mounts, so a channel opened again shows the unread state of that moment.

`MessagePaginator.jumpToTheFirstUnreadMessage()` goes to `firstUnreadMessageId` when there is one. Otherwise it goes to the first message after `lastReadAt`, loading a page around that time if needed, and as a last resort to `lastReadMessageId`. It reads the snapshot first and the user's entry in `channel.state.read` second. With `unreadReferencePolicy: "read-state-only"`, it reads only `channel.state.read`. `Channel` calls it on mount when `initialScrollToFirstUnreadMessage` is set and the channel has unread messages, and the unread banner of the message list calls it when tapped.

`MessagePaginator.seedUnreadSnapshot()` and `MessagePaginator.jumpToTheFirstUnreadMessage()` read the channel's read state in a thread's list too, not the thread's.

`liveViewState` holds `{ isViewingLive }`, `false` until something sets it. While it's `true` on a channel's list, a new message in the channel doesn't count as unread for the user. `MessageList` and `MessageFlashList` set it to `true` while the app is in the foreground and the newest message is on screen, and back to `false` when they unmount.

| Member                          | What it does                                                                       |
| ------------------------------- | ---------------------------------------------------------------------------------- |
| `setViewingLive(isViewingLive)` | Says whether the user is viewing the newest messages.                              |
| `isViewingLive`                 | Returns the value at the moment you read it, and doesn't tell you when it changes. |

[Live view and unread](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/#live-view-and-unread) explains both.

## The latest message

`items` holds only the interval on screen. After a jump to an old message, the newest message is in another interval, and a new message from someone else goes there too, so `items` doesn't change. A message the user sends switches the paginator to the newest interval, so they see what they sent.

`aggregateState` holds the newest message of the channel or thread instead, with these fields:

| Field                    | Type                     | What it holds                                                                                                                   |
| ------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `latestMessage`          | `LocalMessage` or `null` | The newest message the client holds, from any loaded interval or from an event. `null` until one is loaded.                     |
| `queriedLatestMessageAt` | `TimestampNS` or `null`  | The time of the newest message as the last channel or thread query reported it, in nanoseconds. That message may not be loaded. |

`latestMessage` updates when a newer message arrives, when that message is edited, reacted to or deleted, and when it's removed, in which case it falls back to the next newest one. For a channel, thread-only replies don't count, and neither do system messages when the channel type skips them for `last_message_at`.

Two getters return them at the moment you read them, and don't tell you when they change:

- `latestMessage` reads `aggregateState.latestMessage`.
- `latestMessageAt` is the later of `latestMessage.created_at` and `queriedLatestMessageAt`, or `null`. It can be later than `latestMessage` when the server has newer messages the client doesn't hold. Channel lists sort by it for `last_message_at`.

`aggregateState` is a store of its own, so it publishes only when the newest message changes, not on every page or scroll. The channel list preview reads `latestMessage` there.

## Thread wiring and minimal initialization

A thread's replies are a `MessagePaginator` with the thread's parent message id. It requests pages from `client.getReplies()`, newest page first, 50 replies a page, and keeps `items` oldest first.

Get a thread from `client.threads.ensure()`, not with `new Thread()`. A thread created that way knows only its parent message, and its reply list is ready at once:

```ts
const thread = client.threads.ensure({ channel, parentMessage: message });

// The reply list and the composer work before anything is loaded.
// For a new thread, items is undefined until its replies load.
console.log(thread.messagePaginator.items);
```

How the first replies arrive depends on where the thread came from:

- **A thread from the thread list or `client.getThreadAndHydrate()`** comes with its latest replies, and its list starts with them.
- **A thread from `client.threads.ensure()`** has no replies yet. When its parent message has replies, or its channel isn't watched, it's stale. It loads when it's activated, while `client.threads` handles events: `Thread.reload()` fetches the thread and fills the list.

The `Thread` component also calls `MessagePaginator.toTail()` when the list has no replies and nothing is loading, so a thread that isn't stale loads its first page too.

`Thread.reload()` keeps replies that failed to send. [Thread and thread manager](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/thread-and-thread-manager/) lists the thread's API.

## With offline support

With [offline support](https://getstream.io/chat/docs/sdk/react-native/v10/state/offline-support/offline-support/), the local database changes how a channel's list fills:

- A channel list loaded from the database places each channel's stored newest messages in its message list, unless the channel is active, so a channel opens on them before the server answers. Replies that are only in their thread aren't stored.
- `Channel.reload()` also keeps the messages that failed to send and are kept in the database.
- When a reload finds that loaded messages were hard-deleted on the server, it removes them from the database too.

## See also

- [Message paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/) explains intervals, jumping, unread tracking and throttling.
- [Channel manager and ChannelPaginator](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/channel-manager-and-channelpaginator/) lists what every list has.
- [Pinned messages paginator](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/pinned-messages-paginator/) lists the API of a channel's pinned messages.
- [Message operations](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/message-operations/) explains how sending, editing and deleting change the list.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/message-paginator/](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/message-paginator/).