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>
);
};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()fromChannelActionContextto avoid rate-limit issues. - Treat
channelUnreadUiStateas UI state, not as the authoritative backend read state. - Keep unread separators and notifications visually consistent across lists.
- Prefer
WithComponentsfor unread UI customization inside aChannelsubtree. - 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:
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
Channelcan mark the active channel read on mount throughmarkReadOnMount.MessageListandVirtualizedMessageListmark the channel read when the user reaches the latest message.UnreadMessagesNotificationprovides buttons for jumping to the first unread message and for marking the channel read.
Reflecting unread state
UnreadMessagesSeparatormarks the unread boundary inside the list.UnreadMessagesNotificationappears when the separator is not visible.NewMessageNotificationappears when new messages arrive while the user is scrolled away from the latest message.ScrollToLatestMessageButtonjumps to the latest loaded messages or to a newer message set.MessageListNotificationsrenders channel notifications and connection status inside the message list.NotificationListrenders client notifications and is mounted by default inMessageListandVirtualizedMessageList.
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
UnreadMessagesSeparatoris rendered immediately below the last read message.UnreadMessagesSeparator.showCountdefaults totrue.UnreadMessagesSeparatorincludes a mark-read button in the default UI.UnreadMessagesNotification.showCountdefaults totrue.NewMessageNotificationandScrollToLatestMessageButtonare rendered separately fromMessageListNotifications.
Channel Read State Handling Customization
Component props
The primary built-in prop for unread behavior is:
| Component | Prop |
|---|---|
Channel | markReadOnMount |
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.