This is beta documentation for Stream Chat React SDK v14. For the latest stable version, see the latest version (v13) .

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:

PropertyTypeDescription
last_readDateDate when the channel was marked read the last time.
unread_messagesnumberUnread foreign-message count used by the SDK UI.
first_unread_message_idstring | undefinedMessage marked unread through notification.mark_unread.
last_read_message_idstring | undefinedMessage ID preceding the first unread message.

Access The Read State

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

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:

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.

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

Default Components Involved In Read State

Marking a channel read

Reflecting unread state

Marking a channel unread

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

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

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 and ScrollToLatestMessageButton are rendered separately from MessageListNotifications.

Channel Read State Handling Customization

Component props

The primary built-in prop for unread behavior is:

ComponentProp
ChannelmarkReadOnMount

Custom components

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

Custom UnreadMessagesSeparator

import {
  Channel,
  ChannelHeader,
  MessageInput,
  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 />
        <MessageInput />
      </Window>
      <Thread />
    </Channel>
  </WithComponents>
);

Custom UnreadMessagesNotification

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

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

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 MessageListNotifications only when you need to change the channel-notification container itself.

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.