# Channel Read State

This guide provides an overview of how channel read state is handled by default and how to customize the unread UI.

## Best Practices

- Use `markRead()` from `ChannelActionContext` to avoid rate-limit issues.
- Treat `channelUnreadUiState` as UI state, not as the authoritative backend read state.
- Keep unread separators and notifications visually consistent across lists.
- Prefer `WithComponents` for unread UI customization inside a `Channel` subtree.
- Remember that thread replies do not contribute to channel unread counts.

## The Model

The SDK keeps channel unread UI state in `channelUnreadUiState` inside `ChannelStateContext`. It powers the unread separator and unread notifications rendered in `MessageList` and `VirtualizedMessageList`.

The backend read state is still available via `channel.state.read`.

### Channel UI unread state

`channelUnreadUiState` has the following shape:

| Property                  | Type                  | Description                                               |
| ------------------------- | --------------------- | --------------------------------------------------------- |
| `last_read`               | `Date`                | Date when the channel was marked read the last time.      |
| `unread_messages`         | `number`              | Unread foreign-message count used by the SDK UI.          |
| `first_unread_message_id` | `string \| undefined` | Message marked unread through `notification.mark_unread`. |
| `last_read_message_id`    | `string \| undefined` | Message ID preceding the first unread message.            |

## Access The Read State

Use `useChannelStateContext()` to access both the backend `read` mapping and the UI-focused `channelUnreadUiState`:

```tsx
import { useChannelStateContext, useChatContext } from "stream-chat-react";

const ReadStateInspector = ({ user }) => {
  const { client } = useChatContext();
  const { channel, channelUnreadUiState, read } = useChannelStateContext();

  const userReadState = read[user.id];
  const ownReadState = client.user ? read[client.user.id] : undefined;
  const unreadCount = channel.unreadCount();

  return (
    <pre>
      {JSON.stringify({
        channelUnreadUiState,
        ownReadState,
        unreadCount,
        userReadState,
      })}
    </pre>
  );
};
```

## Mark A Channel Read

Use `markRead()` from `ChannelActionContext`:

```tsx
import { useChannelActionContext } from "stream-chat-react";

const MarkReadButton = () => {
  const { markRead } = useChannelActionContext();

  return <button onClick={() => markRead()}>Mark read</button>;
};
```

`markRead()` accepts an optional `updateChannelUiUnreadState` flag if you need to control whether the local unread UI state changes immediately.

<admonition type="tip">

Prefer `markRead()` inside `Channel` children because the SDK already throttles the underlying API calls.

</admonition>

## Default Components Involved In Read State

### Marking a channel read

- [`Channel`](/chat/docs/sdk/react/components/core-components/channel/) can mark the active channel read on mount through `markReadOnMount`.
- [`MessageList`](/chat/docs/sdk/react/components/core-components/message-list/) and [`VirtualizedMessageList`](/chat/docs/sdk/react/components/core-components/virtualized-list/) mark the channel read when the user reaches the latest message.
- [`UnreadMessagesNotification`](/chat/docs/sdk/react/components/contexts/component-context#unreadmessagesnotification/) provides buttons for jumping to the first unread message and for marking the channel read.

### Reflecting unread state

- [`UnreadMessagesSeparator`](/chat/docs/sdk/react/components/contexts/component-context#unreadmessagesseparator/) marks the unread boundary inside the list.
- [`UnreadMessagesNotification`](/chat/docs/sdk/react/components/contexts/component-context#unreadmessagesnotification/) appears when the separator is not visible.
- [`NewMessageNotification`](/chat/docs/sdk/react/components/contexts/component-context#newmessagenotification/) appears when new messages arrive while the user is scrolled away from the latest message.
- [`ScrollToLatestMessageButton`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/MessageList/ScrollToLatestMessageButton.tsx) jumps to the latest loaded messages or to a newer message set.
- [`NotificationList`](/chat/docs/sdk/react/components/contexts/component-context#notificationlist/) renders client notifications and is mounted by default in `MessageList` and `VirtualizedMessageList`.

### Marking a channel unread

The default `MessageActions` menu can expose `Mark as unread` only for foreign messages, and only when the connected user has the required permissions and the action is valid for the message.

<admonition type="note">

Threads do not participate in channel unread counting. The unread separator and unread notifications are not rendered in thread lists.

</admonition>

## Default Unread UI Behavior

- `UnreadMessagesSeparator` is rendered immediately below the last read message.
- `UnreadMessagesSeparator.showCount` defaults to `true`.
- `UnreadMessagesSeparator` includes a mark-read button in the default UI.
- `UnreadMessagesNotification.showCount` defaults to `true`.
- `NewMessageNotification`, `UnreadMessagesNotification`, and `ScrollToLatestMessageButton` are rendered separately from `NotificationList`.

## Channel Read State Handling Customization

### Component props

The primary built-in prop for unread behavior is:

| Component                                                             | Prop              |
| --------------------------------------------------------------------- | ----------------- |
| [`Channel`](/chat/docs/sdk/react/components/core-components/channel/) | `markReadOnMount` |

### Custom components

Use `WithComponents` to override the unread UI components inside a `Channel` subtree.

#### Custom `UnreadMessagesSeparator`

```tsx
import {
  Channel,
  ChannelHeader,
  MessageComposer,
  MessageList,
  Thread,
  UnreadMessagesSeparator,
  Window,
  WithComponents,
  type UnreadMessagesSeparatorProps,
} from "stream-chat-react";

const CustomUnreadMessagesSeparator = (props: UnreadMessagesSeparatorProps) => (
  <UnreadMessagesSeparator {...props} showCount />
);

const App = () => (
  <WithComponents
    overrides={{ UnreadMessagesSeparator: CustomUnreadMessagesSeparator }}
  >
    <Channel>
      <Window>
        <ChannelHeader />
        <MessageList />
        <MessageComposer />
      </Window>
      <Thread />
    </Channel>
  </WithComponents>
);
```

#### Custom `UnreadMessagesNotification`

```tsx
import {
  Channel,
  UnreadMessagesNotification,
  WithComponents,
  type UnreadMessagesNotificationProps,
} from "stream-chat-react";

const CustomUnreadMessagesNotification = (
  props: UnreadMessagesNotificationProps,
) => <UnreadMessagesNotification {...props} queryMessageLimit={50} showCount />;

<WithComponents
  overrides={{
    UnreadMessagesNotification: CustomUnreadMessagesNotification,
  }}
>
  <Channel>{/* ... */}</Channel>
</WithComponents>;
```

#### Custom `NewMessageNotification`

```tsx
import {
  Channel,
  NewMessageNotification,
  WithComponents,
  type NewMessageNotificationProps,
} from "stream-chat-react";

const CustomNewMessageNotification = (props: NewMessageNotificationProps) => (
  <NewMessageNotification {...props} />
);

<WithComponents
  overrides={{ NewMessageNotification: CustomNewMessageNotification }}
>
  <Channel>{/* ... */}</Channel>
</WithComponents>;
```

#### Custom `NotificationList`

```tsx
import {
  Channel,
  NotificationList,
  WithComponents,
  type NotificationListProps,
} from "stream-chat-react";

const CustomNotificationList = (props: NotificationListProps) => (
  <NotificationList {...props} verticalAlignment="top" />
);

<WithComponents overrides={{ NotificationList: CustomNotificationList }}>
  <Channel>{/* ... */}</Channel>
</WithComponents>;
```

Use `MessageListMainPanel` when you need to rearrange the list and notification region together, and keep `NotificationList` for emitted client notifications.

## Jumping To The First Unread Message

The default `UnreadMessagesNotification` uses `jumpToFirstUnreadMessage()` from `ChannelActionContext`. If the first unread message is not loaded locally, the SDK fetches the required message set before scrolling there.


---

This page was last updated at 2026-05-22T16:32:14.314Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/guides/channel-read-state/](https://getstream.io/chat/docs/sdk/react/guides/channel-read-state/).