# Message 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/).

Every channel and every thread has a message paginator. A channel's messages are in `channel.messagePaginator`, and a thread's replies are in `thread.messagePaginator`.

A message paginator is a paginator like any other, as [How pagination works](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/how-pagination-works/) describes. It loads the newest messages first, and each later page loads older ones.

`paginator.items` holds the messages in the order a message list shows them, oldest first.

A thread's paginator loads 50 messages a page. A channel's loads 100 by default, but `Channel` sets it to 25 when it mounts, unless a page size is set in `client.config`. See [What the React Native SDK sets](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/configuration/#what-the-react-native-sdk-sets).

This page covers what's particular to message paginators: jumping to a message and the intervals it creates, unread tracking, the default update throttle and the memory cap, and messages that aren't sent yet. Their methods and options are listed in [Message paginator](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/message-paginator/). How `MessageList` renders them is on [Message list UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/message-list-ui/).

## Intervals after a jump

"Design" has 2,000 messages. When the user opens it, the paginator loads the newest page. Then they follow a link to a message from last month:

1. `MessagePaginator.jumpToMessage()` loads a page around that message. The page is a separate **interval**, because the messages between it and the newest ones aren't loaded.
2. The paginator makes that interval active. `paginator.state.items` holds the interval's messages, and `paginator.state.hasMoreHead` is `true`, because newer messages exist between that interval and the newest one.
3. Bob sends a message. It goes to the newest interval, not to the one on screen, so `items` doesn't change.
4. Scrolling down calls `MessagePaginator.toHead()`, which loads newer messages. When the two intervals meet, they join.
5. `MessagePaginator.jumpToTheLatestMessage()` switches back to the newest interval. It needs no request, because that interval is still loaded.

A message the user sends is different. The paginator switches to the newest interval, so they see what they sent.

`items` holds only the interval on screen. The newest message of the channel or thread, whichever interval is shown, is `messagePaginator.latestMessage`, and its time is `latestMessageAt`. Both are reactive through `messagePaginator.aggregateState`, whose `latestMessage` field holds the message. It skips replies that are only in their thread, and system messages when the channel type says so. A channel list row reads it there.

`latestMessage` is the newest message the client holds, which isn't always the newest on the server. In a channel opened at its first unread message, the newer messages may not be loaded yet, and `latestMessageAt` is then later than the `created_at` of `latestMessage`.

## Jumping and highlighting

Three methods move the paginator to a message:

- **`MessagePaginator.jumpToMessage(messageId)`** shows the interval that holds the message. A loaded message needs no request. Otherwise the paginator loads a page around it.
- **`MessagePaginator.jumpToTheLatestMessage()`** shows the newest interval. When that interval isn't loaded, it loads the newest page, so it gets there in one request however far away the newest messages are. It doesn't highlight anything unless you pass `suppressFocusSignal: false`.
- **`MessagePaginator.jumpToTheFirstUnreadMessage()`** goes to the first message the user hasn't read, as the next section explains.

Each one resolves to `true` when it got there and `false` when it didn't. When `MessagePaginator.jumpToMessage()` or `MessagePaginator.jumpToTheLatestMessage()` fails, it also publishes an error notification through [`client.notifications`](https://getstream.io/chat/docs/sdk/react-native/v10/state/publishing-notifications/). A jump that a later jump or reload overtakes resolves to `false` without a notification. Its page stays loaded but isn't shown.

They take the same options:

- **`pageSize`**: how many messages to load around the target;
- **`focusReason`**: the reason the signal carries, `"jump-to-message"` by default. The other two methods set their own, `"jump-to-latest"` and `"jump-to-first-unread"`;
- **`focusSignalTtlMs`**: how long the highlight lasts once the message is on screen, 3,000 ms by default;
- **`suppressFocusSignal`**: `true` jumps without highlighting;
- **`watchChannel`**: `true` also watches the channel, with the same request, if it isn't watched yet.

After a jump, the `messagePaginator.messageFocusSignal` store says which message to highlight, as `{ signal: { messageId, reason, token, createdAt, ttlMs } }`. The `token` changes on every jump, so jumping twice to the same message gives two signals. The signal stays until it's cleared. Once the message is on screen, call `MessagePaginator.scheduleMessageFocusSignalClear({ token })`, and the signal clears after `ttlMs`.

The SDK's message lists do this for you. They scroll to the message, highlight it, and schedule the clear once it's on screen. They also clear the signal when they unmount. A list of your own follows the signal the same way:

```ts
const { messagePaginator } = channel;

const unsubscribe = messagePaginator.messageFocusSignal.subscribeWithSelector(
  ({ signal }) => ({ signal }),
  ({ signal }) => {
    if (!signal) return;
    // Scroll to signal.messageId and highlight it, then start counting the highlight's lifetime.
    console.log(`Highlight ${signal.messageId} (${signal.reason})`);
    messagePaginator.scheduleMessageFocusSignalClear({ token: signal.token });
  },
);

await messagePaginator.jumpToMessage(messageId);
```

`MessagePaginator.clearMessageFocusSignal()` clears the signal at once, for example when the user scrolls away before the highlight ends. Pass the signal's `token` when the clear can run late. `MessagePaginator.clearMessageFocusSignal({ token })` does nothing if another jump has replaced the signal in the meantime.

## Live view and unread

The state layer has no viewport. A UI that knows the user is looking at the newest messages says so with `MessagePaginator.setViewingLive(true)`, and with `false` when they scroll away or the app goes to the background. The `liveViewState` store holds the value, `{ isViewingLive }`, which is `false` until something sets it. While a channel's list has it `true`, a new message in the channel doesn't count as unread for the user, because the UI is about to mark it read. A thread's list holds the value too, but a thread counts unread replies by whether the thread is active instead.

The SDK's message lists set it for you. It's `true` while the app is in the foreground and the newest message is on screen, and `false` once the list unmounts.

Unread state has a second problem. The user opens "Design" with 12 unread messages, and `Channel` marks the channel read as it opens. Their unread count in [read state](https://getstream.io/chat/docs/sdk/react-native/v10/state/channels-and-threads/read-state-and-unread-counts/) drops to 0, but the UI still needs to show where they left off.

That's what the **unread snapshot** is for. The `unreadStateSnapshot` store keeps a copy of the user's unread state, taken when the paginator loads its first page:

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

Marking the channel read as it opens doesn't change the snapshot. The SDK's message lists reset it as they mark the channel read once the user has caught up, for example when the user dismisses the unread messages notification, or when a new message arrives while the newest messages are on screen. `MessagePaginator.clearUnreadSnapshot()` empties it. These change it too:

- a new message that counts as unread raises `unreadCount`;
- `notification.mark_unread` sets every field from the user's new read state;
- truncating the channel clears it;
- `MessagePaginator.seedUnreadSnapshot()` takes it again from the current read state, and `MessagePaginator.setUnreadSnapshot()` changes some fields. `Channel` calls `MessagePaginator.seedUnreadSnapshot()` each time it opens a channel, because a channel opened again keeps its loaded messages and loads no first page.

`MessagePaginator.jumpToTheFirstUnreadMessage()` reads the snapshot first. It goes to `firstUnreadMessageId` when there is one. Otherwise it goes to the first message after `lastReadAt`, loading a page around that time if needed. As a last resort it goes to the last read message. `Channel` calls it when it opens a channel with unread messages if its `initialScrollToFirstUnreadMessage` prop is `true`.

To make `MessagePaginator.jumpToTheFirstUnreadMessage()` read `channel.state.read` instead of the snapshot, set `unreadReferencePolicy` to `"read-state-only"`. It's read when a channel or thread is created, so register it before, for example in the tree you create the client with:

```ts
client.config.set({
  messagePaginator: { unreadReferencePolicy: "read-state-only" },
});
```

## Throttling and the memory cap

A busy channel can get dozens of events a second: new messages, reactions, read receipts. A paginator that published every one would make the UI render as often. So a message paginator publishes `items` at most once every 500 ms, and a burst of events causes about two updates a second.

Not everything waits:

- loading pages and jumping publish at once;
- changes the user makes through [message operations](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/message-operations/), such as sending, editing or reacting, publish at once.

The `stateThrottleMs` setting sets that wait, 500 ms by default. `0` turns the throttle off, so every change publishes at once.

A long session in a busy channel also keeps adding messages to memory. `maxLoadedItems` caps the newest interval. Once it holds more messages than the cap, the oldest ones are dropped as new ones arrive. `hasMoreTail` becomes `true` again, so scrolling back loads them again. The cap is off by default. It works like this:

- it applies only to the newest interval, while that interval is on screen;
- a cap smaller than `pageSize` counts as `pageSize`;
- unsent messages are never dropped;
- a UI can pause it while the user reads near the oldest loaded message, with `MessagePaginator.setPruningSuspended(true)`, so messages don't disappear under them. The SDK's message lists pause it while the user is within a fifth of the cap from the oldest loaded message.

The cap suits long sessions in busy channels where the user mostly follows the newest messages, such as a livestream. It holds only while the user stays there:

- a jump to an older message leaves the newest interval inactive, so it isn't pruned and keeps growing until the user returns, and the interval jumped to isn't capped either;
- pruning drops the oldest loaded messages when a new one arrives, including pages the user just scrolled back to. The SDK's message lists pause it near the oldest loaded message, as above. A message list of your own calls `MessagePaginator.setPruningSuspended(true)` while the user reads there, and `false` when they return.

These settings go in `client.config`. The `messagePaginator` key applies to channels and threads, and the `messagePaginator` inside `channel` or `thread` overrides it for one of them:

```ts
client.config.set({
  // every message paginator
  messagePaginator: { stateThrottleMs: 300 },
  // channels only: a livestream keeps at most 500 messages, loaded 25 at a time
  channel: { messagePaginator: { maxLoadedItems: 500, pageSize: 25 } },
});
```

A change to these keys rebuilds the settings of every channel's message paginator, and drops the page size `Channel` set on it. Name `pageSize` here, as above, to keep the one you want.

## Unsent messages

A message paginator also holds the user's messages that the server hasn't confirmed yet:

- **Sending.** A message the user sends appears at once with status `sending`, without waiting for the throttle. When the server answers, its copy replaces the local one and the status becomes `received`.
- **Failed.** If the request fails, the message stays in the paginator with status `failed`, and can be retried. `Channel.reload()` and `Thread.reload()` keep failed messages. With [offline support](https://getstream.io/chat/docs/sdk/react-native/v10/state/offline-support/offline-support/), a send that fails because the device is offline is also kept as a pending task, and sent again after the next reconnect.
- **Waiting for uploads.** With [sending with pending uploads](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/sending-with-pending-uploads/) on, a message is sent before its attachments finish uploading. It shows at once with its local files. Each attachment keeps its `localMetadata`, with `uploadState` and `uploadProgress` as they were when the user sent the message. When the uploads finish, the message goes out. If one fails, the message fails. `Chat` turns this on when offline support is on.

The memory cap never drops these messages, and they never mark where older pages start loading.

[Message operations](https://getstream.io/chat/docs/sdk/react-native/v10/state/writing-messages/message-operations/) explains sending and retrying. [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 what each message action shows before the server answers.

## Used in

- [Read state, unread and receipts](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/read-state-unread-and-receipts/)
- [Replies and threads](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/replies-and-threads/)
- [Sending with pending uploads](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/sending-with-pending-uploads/)

---

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