Reading state in components
A component reads state with useStateStore(store, selector). The hook subscribes to one store of a state-layer object, and re-renders the component when the values the selector picks change. What a store is, and which objects have one, is on Reactive state. Getting the object comes first: Providers and context hooks explains which client, channel, thread or composer a component gets.
Here is an unread badge for a channel list row:
import { Text } from "react-native";
import type { Channel, ChannelStateData } from "stream-chat";
import { useChatContext, useStateStore } from "stream-chat-react-native";
const readSelector = (state: ChannelStateData) => ({ read: state.read });
// Rendered in a custom ChannelPreview, once for each channel list row.
export const UnreadBadge = ({ channel }: { channel: Channel }) => {
const { client } = useChatContext();
const { read } = useStateStore(channel.state, readSelector);
const unread = client.userId
? (read[client.userId]?.unread_messages ?? 0)
: 0;
if (!unread) return null;
return <Text>{unread}</Text>;
};channelis the channel of the row the badge renders in: theChannelPreviewoverride gets it as a prop, and passes it on. Providers and context hooks explains where each component gets its channel.readSelectorpicks thereadfield fromchannel.state, which holds every member's read state.useStateStoresubscribes tochannel.state. When the channel's state changes, it runs the selector again and re-rendersUnreadBadgeonly ifreadis a different value.- The component works out the current user's count from
readafter selecting it.
The useStateStore rules
- Define selectors at module scope. The hook resubscribes whenever it gets a different selector function. When a selector needs a prop, wrap it in
useCallback. - Return an object with fixed keys. Write
(state) => ({ items: state.items }). Never return the slice itself ((state) => state.items), and never a primitive ((state) => state.isLoading): the hook compares the returned object key by key. - Derive values after selecting. Select the fields as they are and compute from them in the component, as
UnreadBadgedoes. A selector that maps or filters builds a new array on every run, so the component re-renders on every change to the store. - Import
useStateStorefromstream-chat-react-native(orstream-chat-expo). It is the one the SDK's components use.
The store you pass can be undefined, for example thread?.state in a component that also renders outside a thread. The hook then returns undefined instead of the selection, so you don't need to call it conditionally:
import { Text } from "react-native";
import type { ThreadState } from "stream-chat";
import { useStateStore, useThreadContext } from "stream-chat-react-native";
const replyCountSelector = (state: ThreadState) => ({
replyCount: state.replyCount,
});
export const ReplyCount = () => {
const { thread } = useThreadContext();
// Outside a thread, the store is undefined and so is the selection.
const { replyCount } = useStateStore(thread?.state, replyCountSelector) ?? {};
if (replyCount === undefined) return null;
return <Text>{replyCount} replies</Text>;
};A selector that depends on a prop:
import { useCallback } from "react";
import { Text } from "react-native";
import type { ChannelStateData } from "stream-chat";
import { useChannel, useStateStore } from "stream-chat-react-native";
export const MemberRole = ({ userId }: { userId: string }) => {
const channel = useChannel();
const selector = useCallback(
(state: ChannelStateData) => ({ member: state.members[userId] }),
[userId],
);
const { member } = useStateStore(channel.state, selector);
return <Text>{member?.channel_role ?? "not a member"}</Text>;
};Pitfalls
Selector identity
An inline selector is a new function on every render, so the hook unsubscribes and subscribes again each time:
import { Text } from "react-native";
import type { ChannelStateData } from "stream-chat";
import { useChannel, useStateStore } from "stream-chat-react-native";
// Resubscribes on every render.
export const TypingCountInline = () => {
const channel = useChannel();
const { typing } = useStateStore(
channel.state,
(state: ChannelStateData) => ({
typing: state.typing,
}),
);
return <Text>{Object.keys(typing).length}</Text>;
};
// Subscribes once.
const typingSelector = (state: ChannelStateData) => ({ typing: state.typing });
export const TypingCount = () => {
const channel = useChannel();
const { typing } = useStateStore(channel.state, typingSelector);
return <Text>{Object.keys(typing).length}</Text>;
};The same applies to the filter you pass to useNotifications and useSystemNotifications: the hook builds its selector from it, so an inline filter resubscribes on every render too. Define it at module scope.
Returning the slice itself
A selector such as (state) => state.members compiles, but the component doesn't re-render on every change, for example when a member leaves. Wrap the slice in an object, (state) => ({ members: state.members }), as rule 2 says.
Two useStateStore hooks
@stream-io/state-store, the package the stores come from, has its own useStateStore in @stream-io/state-store/react-bindings. The one in stream-chat-react-native compares the keys of the previous selection with ===, and the one in @stream-io/state-store compares the keys of the new selection with Object.is. With selectors that follow the rules above, both see the same changes. Import the one from stream-chat-react-native.
Getters that aren't reactive
Many objects have getters that read a store's current value. Reading one during render subscribes to nothing: the component shows the new value only when something else re-renders it.
| Getter | Subscribe to |
|---|---|
channel.data |
channel.state, field data |
channel.state.unreadCount |
channel.state, field read, as in UnreadBadge |
channel.state.members, .read, .typing, .watchers |
channel.state, the field of the same name |
thread.ownUnreadCount |
thread.state, field read (unreadMessageCount per user) |
messagePaginator.isViewingLive |
messagePaginator.liveViewState |
client.user |
no store holds the current user yet |
Store names vary
Most stores are called state, but not all of them:
channel.stateis itself the channel's store. There is nochannel.state.state.client.notifications.storeholds the notifications.client.mutedUsersStoreholds the users the current user has muted.useMutedUsers()reads it.channel.messageReceiptsTracker.snapshotStoreholds delivery and read receipts.configStateholds an object's settings, on the channel, the composer, the paginators and the other configurable objects.- A message paginator has extra stores next to
state:unreadStateSnapshot(the unread boundary),liveViewState,aggregateState(the latest message) andmessageFocusSignal(the message to scroll to and highlight). - The thread list is
client.threads.paginator.state.client.threads.stateholds the unread thread count and the unseen threads.
The fields of each store are in Reference.
Context hooks that throw
useChannel(), useChannelContext() and useMessagePaginator() throw outside a Channel or a Thread, and so do the hooks built on them. useChatContext() throws outside Chat. A component that renders both inside and outside a channel, a channel list row for example, takes the channel as a prop instead: the channel hooks such as useChannelName(channel) and useChannelMemberCount(channel) accept it as an argument. Providers and context hooks lists which hook throws where.
Batched message-list updates
A message paginator's state publishes at most once every 500 ms by default, so a burst of events causes about two renders a second instead of one per event. The user's own sends skip the wait and show at once. A component that reads state.items can therefore lag up to half a second behind an event. To react to each event as it arrives, listen with Channel.on() (see Events). The interval is a setting of the paginator: see Message paginators.
Hooks by state area
Every hook is documented in Reference, one page per state area:
- State and entity hooks
- Loading data hooks
- Channel and thread hooks
- Channel preview hooks
- Channel details hooks
- Connection hooks
- Composer hooks
- Message hooks
- Notification hooks
- Reminder hooks
- Poll hooks
- Location hooks
- AI hooks
Where no hook covers a store, read it with useStateStore.