# Message list UI

> **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/).

The message list is the scrolling part of a channel or a thread. It renders what a message paginator holds and turns the user's scrolling into paginator calls. This page covers the two lists, what they show after a jump, the separators and floating controls, and how one message is put together. The props are in the [MessageList](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message-list/messagelist/) and [MessageFlashList](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message-list/messageflashlist/) references.

## MessageList and MessageFlashList

Both lists render the messages of the channel or thread they are in:

- Inside a `Thread`, they render the thread's replies, `thread.messagePaginator`, with the parent message above them.
- Otherwise they render the channel's messages, `channel.messagePaginator`.

So the same `<MessageList />` works in both places. Each list subscribes to its paginator's state, and:

- loads older messages when the user scrolls within 100 points of the oldest loaded message;
- loads newer messages when the user scrolls within 100 points of the newest loaded message of an interval that isn't the latest one;
- tells the paginator whether the user is viewing the latest messages: the app is in the foreground, the latest messages are loaded, and the newest one is on screen. While that holds, a new message doesn't count as unread, and in a channel the list marks the channel read when one arrives. [Read state, unread and receipts](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/read-state-unread-and-receipts/) covers the rules.

A load in one direction waits for a load in flight in the other, so the two never shift the scroll position under each other. How the paginator holds intervals of messages is on [Message paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/).

The two lists differ in what they're built on:

|                                | `MessageList`                                        | `MessageFlashList`                                                               |
| ------------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------------------- |
| Built on                       | React Native's `FlatList`, inverted                  | `FlashList` from `@shopify/flash-list`, which you install                        |
| Status                         | Stable                                               | Experimental: its implementation can change                                      |
| Extra list props               | `additionalFlatListProps`                            | `additionalFlashListProps`                                                       |
| Prop for the older end         | `FooterComponent`                                    | `HeaderComponent`                                                                |
| Prop for the newer end         | `HeaderComponent`                                    | `FooterComponent`                                                                |
| A list shorter than the screen | Loads older pages until the messages fill the screen | Loads older pages as the user scrolls up                                         |
| Cells                          | Created for each message                             | Recycled between messages of the same shape, such as text only or with a gallery |

Use `MessageList` unless profiling shows the list is the bottleneck. `MessageFlashList` throws when `@shopify/flash-list` isn't installed. The list props are named after the list's own ends: `MessageList` is inverted, so its footer is at the top of the screen.

## Jumps on screen

A jump moves the list to one message: a quoted message the user taps, a search result, a pinned message, the first unread message or the latest one. The paginator loads the interval of messages around the target, and the list shows it:

1. The interval replaces what was on screen, and once the target is rendered, the list scrolls it to the middle.
2. The target is highlighted. The highlight lasts 3 seconds, `DEFAULT_HIGHLIGHT_DURATION`, counted from when the list scrolled to it.
3. If the interval isn't the latest one, the scroll-to-bottom button shows, and scrolling down loads newer messages.

A jump is a paginator call, so you start one the same way from anywhere: `MessagePaginator.jumpToMessage(messageId, { focusSignalTtlMs: DEFAULT_HIGHLIGHT_DURATION })` on `channel.messagePaginator`, or on `thread.messagePaginator` for a reply. The list reacts to the paginator's `messageFocusSignal`, even when the jump started before the list mounted. The jump itself is on [Message paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/message-paginators/), and the places that start one are on [Moving between channels, threads and messages](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/moving-between-channels-threads-and-messages/).

## Floating controls

Over the list, three elements tell the user where they are:

- **The unread notification**, `UnreadMessagesNotification`, sits at the top of a channel's list. It shows "5 unread" while every message on screen is newer than the last message the user read, so that message is above the screen. Tapping it jumps to the first unread message, and its close button marks the channel read. It doesn't show in a thread, in a channel without read events (unless the channel counts unread messages locally), or while the attachment picker shows the photo grid.
- **The scroll-to-bottom button**, `ScrollToBottomButton`, shows while the list isn't at the latest message, or while the latest messages aren't loaded. In a channel, its badge shows the user's unread count from the channel's read state; in a thread, it shows no badge. Tapping it scrolls to the newest message, or jumps to the latest messages when they aren't loaded. In a channel it then marks the channel read, and the unread separator stays where it was.
- **The sticky date header**, `StickyHeader`, sits at the top and shows the date of the topmost message on screen. It hides when the oldest message of the channel is on screen, where the inline date separator says the same. `hideStickyDateHeader` turns it off.

`NetworkDownIndicator` shows "Waiting for network..." while the device is offline, and "Reconnecting..." while the connection to Stream is down.

The list also hosts two things that float over it but belong to other parts. The composer's suggestion list renders here, just above the composer, so a screen with a `MessageComposer` and no message list shows no suggestions. The `NotificationList` of the channel or thread renders here too; see [Displaying notifications](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/displaying-notifications/).

## Separators

**Date separators** mark where a day starts. An `InlineDateSeparator` goes above the first message of each day. System messages, such as "Cleo joined the channel", get one only with `allowDateSeparatorForSystemMessages`. `hideDateSeparators` turns them off. The separator's date format, and the sticky header's, are translation keys; [Localization](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/) covers them.

**The unread separator**, `InlineUnreadIndicator`, marks where the unread messages start, with "5 new messages". It renders under the last message the user read, from the paginator's unread snapshot: the unread state when the user opened the channel. Opening the channel marks it read but keeps the snapshot, so the user can still see where the new messages start. The snapshot clears when the user closes the unread notification, and when a new message arrives while the user is viewing the latest messages. `shouldShowUnreadUnderlay={false}` on `Channel` turns the separator off. [Read state and unread counts](https://getstream.io/chat/docs/sdk/react-native/v10/state/channels-and-threads/read-state-and-unread-counts/) explains the snapshot.

Messages from the same user, sent one after another, are grouped: the sender's avatar and the timestamp show on the last message of the group. A message that is still sending or that failed keeps its footer, so its status stays visible. `enableMessageGroupingByUser={false}` on `Channel`, or `noGroupByUser` on the list, turns grouping off, and `maxTimeBetweenGroupedMessages` starts a new group after a gap.

## Loading, empty and error states

| State                      | What the list renders                                                                                       | Override key                  |
| -------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------- |
| Loading the first page     | A spinner with "Loading messages..." in place of the list                                                   | `MessageListLoadingIndicator` |
| Loading older messages     | A spinner at the older end. In a thread, under the parent message.                                          | None (the list's prop)        |
| Loading newer messages     | A spinner at the newer end                                                                                  | None (the list's prop)        |
| A channel with no messages | `EmptyStateIndicator` with `listType="message"`. A thread with no replies shows its parent message instead. | `EmptyStateIndicator`         |
| The first page failed      | `Channel` or `Thread` renders `LoadingErrorIndicator` in place of its children                              | `LoadingErrorIndicator`       |

A failed load of older or newer messages is recorded as `lastQueryError` in the paginator's state, and the list stays as it is. The next scroll to that end tries again.

## A message

For each message, the list renders a row: the date separator when a day starts, the message, and the unread separator after the last read message. A system message renders `MessageSystem` instead of a message.

The message is `Message`. It creates the message's handlers (react, reply, edit, delete, flag, mute, pin, open the thread, retry a failed send), puts them in `MessageContext` with the message, along with `onLongPress`, which opens the message menu. Then it renders the `MessageItemView` override, by default `MessageItemView`, **with no props**. A message view reads everything from `useMessageContext()`.

The default `MessageItemView` is made of parts, each its own override key:

| Part                                                                                   | Override key                                                                                                              |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| The pinned, reminder, saved-for-later and also-sent-in-channel marks above the message | `MessageHeader`, with `MessagePinnedHeader`, `MessageReminderHeader`, `MessageSavedForLaterHeader`, `SentToChannelHeader` |
| The sender's avatar, on other users' messages                                          | `MessageAuthor`                                                                                                           |
| The bubble with its content                                                            | `MessageContent`                                                                                                          |
| A quoted message                                                                       | `Reply`                                                                                                                   |
| Images and videos, files, other attachments                                            | `Gallery`, `FileAttachmentGroup`, `Attachment`                                                                            |
| The text of an AI reply as it streams in                                               | `StreamingMessageView`                                                                                                    |
| Reactions, above or below the bubble                                                   | `ReactionListTop`, `ReactionListBottom`                                                                                   |
| The timestamp, the "Edited" mark and the status                                        | `MessageFooter`, with `MessageTimestamp` and `MessageStatus`                                                              |
| The replies button, in a channel                                                       | `MessageReplies`                                                                                                          |
| A deleted or failed message                                                            | `MessageDeleted`, `MessageError`                                                                                          |

A poll renders in the bubble. `messageContentOrder` on `Channel` sets the order of the content inside the bubble. The text renders Markdown; `markdownRules` on `Channel` changes the rules, and the [Message reference](https://getstream.io/chat/docs/sdk/react-native/v10/component-reference/message/message-ui-components/) covers the parts in detail.

## Replacing parts

Register replacements with `WithComponents`. [Overriding components](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/overriding-components/) explains how overrides apply.

| Override key                                         | Replaces                                                                                     |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `MessageItemView`                                    | A message's look. Gets no props; reads `useMessageContext()`.                                |
| `Message`                                            | A whole message, its handlers included. Gets the message and its place in the list as props. |
| The keys in [A message](#a-message)                  | One part of a message                                                                        |
| `MessageSystem`                                      | A system message                                                                             |
| `InlineDateSeparator`                                | The date separators between messages                                                         |
| `StickyHeader`, `DateHeader`                         | The sticky date header, and the date label it renders                                        |
| `InlineUnreadIndicator`                              | The unread separator                                                                         |
| `UnreadMessagesNotification`                         | The unread notification                                                                      |
| `ScrollToBottomButton`                               | The scroll-to-bottom button. Gets `onPress` and `showNotification`.                          |
| `TypingIndicator`, `TypingIndicatorContainer`        | The typing indicator and its container                                                       |
| `NetworkDownIndicator`                               | The connection banner                                                                        |
| `EmptyStateIndicator`, `MessageListLoadingIndicator` | The empty and first-page loading states                                                      |

The spinners at the two ends are props of the list, not override keys: pass `FooterComponent` and `HeaderComponent` to `MessageList`, or `HeaderComponent` and `FooterComponent` to `MessageFlashList`. In a thread, the default at the older end is `ThreadFooterComponent`, which renders the parent message; a replacement there replaces the parent message too. At the newer end, the default also renders the typing indicator, so a replacement there removes it unless it renders one itself.

This list shows each message as one compact line:

```tsx title="src/CompactMessages.tsx"
import { Pressable, StyleSheet, Text, View } from "react-native";
import {
  MessageList,
  MessageTextContainer,
  useMessageContext,
  WithComponents,
} from "stream-chat-react-native";

const CompactMessage = () => {
  const { contextMenuAnchorRef, message, onLongPress } = useMessageContext();

  return (
    // The default view opens the message menu on a long press; a view of your own does it too.
    <Pressable
      onLongPress={(event) => onLongPress({ emitter: "messageContent", event })}
    >
      <View ref={contextMenuAnchorRef} style={styles.row}>
        <Text style={styles.name}>
          {message.user?.name ?? message.user?.id}
        </Text>
        <MessageTextContainer />
      </View>
    </Pressable>
  );
};

const overrides = { MessageItemView: CompactMessage };

export const CompactMessages = () => (
  <WithComponents overrides={overrides}>
    <MessageList />
  </WithComponents>
);

const styles = StyleSheet.create({
  name: { fontWeight: "600" },
  row: {
    flexDirection: "row",
    gap: 8,
    paddingHorizontal: 16,
    paddingVertical: 4,
  },
});
```

`MessageTextContainer` reads the message from `MessageContext` too, so it needs no props. The message menu opens from the `onLongPress` in the context, which `Message` created. It positions itself on the view in `contextMenuAnchorRef`. The [Custom message UI cookbook](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/message/custom-message/) builds a complete message view.

## Used in

- [Reactions](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/reactions/)
- [Pinned messages](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/pinned-messages/)
- [Read state, unread and receipts](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/read-state-unread-and-receipts/)
- [Custom message](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/message/custom-message/)
- [Date separators](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/message/date-separators/)
- [Livestream message list](https://getstream.io/chat/docs/sdk/react-native/v10/ui-cookbook/message/livestream-message-list/)

---

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