import { useChannelContext, useChatContext } from "stream-chat-react-native";
const Component = (props) => {
const { client } = useChatContext();
const { read, channel, channelUnreadState } = useChannelContext();
// channel read state for some user
const channelReadStateForAUser = read[props.user.id];
// channel read state for own user
const channelReadStateForMyUser = client.user && read[client.user.id];
// easier way to access own user's unread count for a given channel
const unreadCount = channel.countUnread();
//... code
};Channel Read State
This guide explains how the SDK handles channel read state by default and how to customize it.
Best Practices
- Use
markRead()from context to keep UI and backend state aligned. - Avoid overriding unread UI unless you preserve the same behavior.
- Keep unread indicators consistent across channel list and message list.
- Respect throttling to avoid read-rate limits.
- Remember threads don’t affect channel unread counts by default.
The model
The SDK keeps a separate channelUnreadState inside Channel for UI components like InlineUnreadIndicator and UnreadMessagesNotification. When you open a channel and mark it read, channelUnreadState doesn’t immediately reset so users can still see their previous unread count.
The backend read state is available via channel.state.read.
Channel UI unread state
Channel maintains this state and shares it via ChannelContext as channelUnreadState.
| Property | Type | Description |
|---|---|---|
| last_read | Date | Date when the channel was marked read the last time. |
| unread_messages | number | The count of unread messages in a given channel. Unread count refers only to foreign (not own) unread messages. |
| first_unread_message_id | string or undefined | The ID of the message that was marked unread (notification.mark_unread event). The value is available only when a message is marked unread. Therefore, cannot be relied on to place unread messages UI. |
| last_read_message_id | string or undefined | The ID of the message preceding the first unread message. |
Channel read state for all users
The read state comes from the channel query response (ChannelResponse.read) and is transformed from an array into an object keyed by user ID. It updates on WS events like message.read, notification.mark_unread, and message.new.
| Property | Type | Description |
|---|---|---|
| last_read | Date | Date when the channel was marked read the last time. The value is provided with ChannelResponse when querying channels or on notification.mark_unread event. |
| unread_messages | number | The count of unread messages in a given channel for a given user. Unread count refers only to foreign (not own) unread messages. The value is provided with ChannelResponse when querying channels or on notification.mark_unread event. |
| user | user | Data of a user, whose read state is described in this object. The value is provided with ChannelResponse when querying channels or on notification.mark_unread event. |
| first_unread_message_id | string or undefined | The ID of the message that was marked unread (notification.mark_unread event). The value is available only when a message is marked unread. Therefore, cannot be relied on to place unread messages UI. |
| last_read_message_id | string or undefined | The ID of the message preceding the first unread message. The value is provided with ChannelResponse when querying channels or on notification.mark_unread event. |
Access the read state
Access read and channelUnreadState via useChannelContext:
Mark a channel read
Channel can be marked read using the markRead() function provided via ChannelContext:
import { Button } from "react-native";
import { useChannelContext } from "stream-chat-react-native";
const MarkReadButton = (props) => {
const { markRead } = useChannelContext();
return <Button {...props} onPress={() => markRead()} title="Mark read" />;
};markRead accepts an optional options parameter:
| Field | Type | Optional | Description |
|---|---|---|---|
updateChannelUnreadState | Boolean | Yes | Signal, whether the channelUnreadState should be updated. The local state update is prevented when the Channel component is mounted. This is in order to keep the UI indicating the original unread state, when the user opens a channel. If the value for updateChannelUnreadState is not provided, the state is updated. |
Prefer markRead() within Channel context; it throttles API calls to avoid mark-read rate limits.
markRead() respects the current user's read-receipts privacy setting: when user.privacy_settings.read_receipts.enabled is false, the call is a no-op — no read is sent to the backend, so the user's reads aren't shared with others. This is independent of the channel's read-events capability.
Default components involved in handling read state
The default components included in marking a channel read:
| Component | Description |
|---|---|
Channel | Can be configured to mark active channel read when mounted. This can be done through its prop markReadOnMount. By default enabled. |
MessageList | Marks channel read when message list is scrolled to the bottom. |
UnreadMessagesNotification | Floating notification rendered in the message list. Contains a button, which when clicked, takes me to the first unread message. |
The message actions menu includes a “mark as unread” action that updates channel state. See Customizing message actions.
The default components reflecting channel unread count:
Message threads do not participate in handling read state of a channel. Thread replies are not observed for unread count. Therefore, none of the UI components related to read state are rendered in threads.
Default channel read state handling
The channel is marked read in the following scenarios:
- User enters a channel with unread messages if
ChannelpropmarkReadOnMountis enabled (default behavior). - User scrolls up and back down to the latest message.
- User clicks the button on the default
UnreadMessagesNotificationcomponent to jump to the first unread message. - The
ScrollToBottomButtonappears and can take you to the latest message.
The channel is marked unread in the following scenarios:
- User with
read-eventspermission is shownMark as unreadoption in the message actions list.
InlineUnreadIndicator appears below the last read message. It can be followed by your own or another user’s message.
Channels with read events disabled
Some channel types — most commonly livestream — ship with the read-events capability disabled. On these channels the backend does not track per-user read state, so by default the SDK does not maintain an unread count for them at all: incoming messages are never counted as unread, and markRead() has no effect.
If you still want a lightweight “new messages” indicator on such channels, enable a client-local unread count when you create the client:
const client = StreamChat.getInstance("Your API Key", {
isLocalUnreadCountEnabled: true,
});With this option on, channels that have read events disabled keep a purely client-side unread count. It increments as new messages arrive and is exposed through the same APIs as the regular unread count — channel.countUnread() and channel.state.unreadCount — so the existing unread UI components keep working.
This count is never sent to the backend. To reset it, call markReadLocally() on the channel instance:
channel.markReadLocally();markReadLocally() resets the local count without any network request (it dispatches a client-only message.read_locally event). Use it instead of markRead() on read-events-disabled channels — markRead() relies on server-side read events and is a no-op there.
When offline support is enabled, the local count is persisted to the offline database, so it survives app restarts.
Whether a channel tracks reads server-side is derived from its own_capabilities: read events are treated as disabled only when the capabilities are known and exclude read-events (e.g. livestreams); when capabilities are unknown, read events are assumed to be enabled.
Channel read state handling customization
Customization through component props
You can configure when a channel is marked read by tweaking these default component props:
| Component | Prop |
|---|---|
Channel | markReadOnMount (by default enabled) |
Customization through custom components
You can override these components using WithComponents:
Custom InlineUnreadIndicator component
Will be rendered before the first unread message.
import { WithComponents } from "stream-chat-react-native";
const InlineUnreadIndicator = () => {
//... custom implementation
};
const Component = ({ children }) => (
<WithComponents overrides={{ InlineUnreadIndicator }}>
{children}
</WithComponents>
);Custom UnreadMessagesNotification component
Rendered only when UnreadMessagesSeparator is not visible. The default implementation is a floating element that shows unread count since the user scrolled away from the latest message.
import {
WithComponents,
UnreadMessagesNotificationProps,
} from "stream-chat-react-native";
const UnreadMessagesNotification = (props: UnreadMessagesNotificationProps) => {
//... custom implementation
};
const Component = ({ children }) => (
<WithComponents overrides={{ UnreadMessagesNotification }}>
{children}
</WithComponents>
);The component can be configured through the following props:
| Prop | Description | Type | Default |
|---|---|---|---|
onPressHandler | Called when the component is pressed. | Function|undefined | undefined |
onCloseHandler | Called when the close icon is pressed. | Function|undefined | undefined |
Custom ScrollToBottomButton component
The SDK exports ScrollToBottomButton, which shows unread count since the user scrolled away from the newest messages.
We can implement our own message scroll to bottom button component.
import {
WithComponents,
ScrollToBottomButtonProps,
} from "stream-chat-react-native";
const ScrollToBottomButton = (props: ScrollToBottomButtonProps) => {
//... custom implementation
};
const Component = ({ children }) => (
<WithComponents overrides={{ ScrollToBottomButton }}>
{children}
</WithComponents>
);Jumping to the first unread message
Initial scroll to first unread messages on mount
Enable initialScrollToFirstUnreadMessage on Channel.
Default SDK component to jump to the first unread message
UnreadMessagesNotification can scroll to the first unread message. If it’s not loaded, it’s fetched from the API.
API to jump to the first unread message
Use loadChannelAtFirstUnreadMessage() to implement custom UI. It takes limit, channelUnreadState, setChannelUnreadState, and setTargetedMessage, all available from useChannelContext.
Showing total unread counts in the app
To show total unread counts:
- Read the total unread count from the
client.connectUser()response.
const [unreadCount, setUnreadCount] = useState<number>();
const client = StreamChat.getInstance("Your API Key", {
timeout: 6000,
});
const user = {
id: "Your User ID",
};
const connectedUser = await client.connectUser(user, "Your User Token");
const initialUnreadCount = connectedUser?.me?.total_unread_count; // Use on initial load.- Listen for updates via the client event listener:
useEffect(() => {
const listener = client?.on((e) => {
if (e.total_unread_count !== undefined) {
setUnreadCount(e.total_unread_count);
} else {
const countUnread = Object.values(chatClient.activeChannels).reduce(
(count, channel) => count + channel.countUnread(),
0,
);
setUnreadCount(countUnread);
}
});
return () => {
if (listener) {
listener.unsubscribe();
}
};
}, [chatClient]);