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.
Here is an unread badge for the current channel:
import { Text } from "react-native";
import type { ChannelStateData } from "stream-chat";
import {
useChannel,
useChatContext,
useStateStore,
} from "stream-chat-react-native";
const readSelector = (state: ChannelStateData) => ({ read: state.read });
export const UnreadBadge = () => {
const { client } = useChatContext();
const channel = useChannel();
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>;
};useChannel()finds the channel: theChannel's, or the thread's channel inside aThread.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.
Which owner a hook reads
The context hooks find their owner from where the component sits, so the same component works in a channel and in a thread:
| Hook | Inside a Channel |
Inside a Thread |
|---|---|---|
useChannel() |
the channel | the thread's channel |
useThreadContext() |
{ thread: undefined } |
{ thread } |
useMessageComposer() |
channel.messageComposer |
thread.messageComposer |
useMessagePaginator() |
channel.messagePaginator |
thread.messagePaginator |
Two more cases:
- A message being edited. While the user edits a message,
useMessageComposer()returns the composer that edits it, not the channel's or the thread's. - A poll. Inside a poll in a message,
usePollContext()returns{ message, poll }. Read its state withusePollState(), or withuseStateStore(poll.state, selector).
useMessagePaginator() picks the message list the way the SDK's own message lists do: the thread's when there is one, otherwise the channel's.
import { Text } from "react-native";
import type { MessagePaginatorState } from "stream-chat";
import { useMessagePaginator, useStateStore } from "stream-chat-react-native";
const itemsSelector = (state: MessagePaginatorState) => ({
items: state.items,
});
export const LoadedMessageCount = () => {
const paginator = useMessagePaginator();
const { items } = useStateStore(paginator.state, itemsSelector);
return <Text>{items?.length ?? 0} messages loaded</Text>;
};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 may be undefined, for example before the object that holds it exists. The hook then returns undefined, so you don't need a conditional hook call.
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.
Selections that go stale
A selector that returns the slice itself compiles, but misses changes. Take (state) => state.members, an object keyed by user ID:
- An added member is missed.
useStateStorecompares the previous selection's keys, and the previous selection has no key for the new member. - A removed member is missed. The store only notifies the hook when the new selection's keys differ, and the new selection no longer has the removed key.
(state) => state.items fails the same way: a message added at the end, or the last one removed, doesn't re-render the component. Wrap the slice in an object, (state) => ({ members: state.members }), and the comparison is on the slice as a whole.
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.
The right owner inside threads
Inside a Thread, useChannel() returns the thread's channel, so channel.messagePaginator and channel.messageComposer are the channel's messages and composer, not the thread's. Read the composer with useMessageComposer(), and the message list with useMessagePaginator().
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
The hooks below are exported from the SDK. Where no hook covers a store, read it with useStateStore.
State and entities
Reference: State and entity hooks.
| Hook | Reads |
|---|---|
useStateStore |
any store, through your selector |
useChatContext |
client, getAppSettings, isMessageAIGenerated |
useChannel |
the channel of the nearest Channel or Thread |
useChannelContext |
the channel, and the display options set on Channel |
useThreadContext |
the Thread's thread, or { thread: undefined } |
useMutedUsers |
client.mutedUsersStore: the users the current user muted |
useMutedChannels |
client.mutedChannels, re-read on health.check and notification.channel_mutes_updated |
Loading data
Reference: Loading data hooks.
| Hook | Reads |
|---|---|
useChannelsContext |
inside ChannelList, the list's paginator and its configuration |
useMessagePaginator |
the message list of the Thread, otherwise of the Channel |
useIsTargetedMessage |
messageFocusSignal: whether a message is the one jumped to and highlighted |
useQueryReminders |
client.reminders.paginator.state: the reminder list |
For the channels of a list, use useStateStore(paginator.state, selector) on the paginator you pass to ChannelList. For the messages of a message list, use useStateStore on the paginator from useMessagePaginator(), as LoadedMessageCount does. For the thread list, use useStateStore(client.threads.paginator.state, selector).
Channels and threads
Reference: Channel and thread hooks.
| Hook | Reads |
|---|---|
useChannelOwnCapabilities |
channel.state ownCapabilities |
useChannelMembersState |
channel.state members |
useChannelMembershipState |
channel.state membership: the current user's own membership |
useChannelMemberCount |
channel.state memberCount |
useChannelOnlineMemberCount |
channel.state watcherCount |
useChannelName |
channel.state data.custom.name |
useChannelImage |
channel.state data.custom.image |
useIsDirectChat |
whether the channel has exactly one member other than the current user |
useCooldownRemaining |
channel.cooldownTimer.state: seconds left in slow mode |
useMarkRead |
nothing: it returns a markRead() for the channel |
For anything else in channel.state, thread.state or client.threads.state, use useStateStore with your own selector.
Connection
Reference: Connection hooks.
| Hook | Reads |
|---|---|
useWSConnectionState |
client.wsConnection.state: the socket |
useWSConnectionStateSelector |
a selection of client.wsConnection.state |
useSettledWSConnectionHealth |
the socket's health, counting a drop only once it lasts |
useNetworkConnectionState |
client.networkConnection.state: the device's network |
useNetworkConnectionStateSelector |
a selection of client.networkConnection.state |
Writing messages
Reference: Composer hooks.
| Hook | Reads |
|---|---|
useMessageComposer |
the composer: the edit composer, the thread's or the channel's |
useMessageInputContext |
the composer UI's context, provided by Channel and Thread |
useAttachmentManagerState |
attachmentManager.state, and the composer's upload counts |
useMessageComposerHasSendableData |
whether there is something to send |
usePendingUploadsEnabled |
whether a message can be sent before its uploads finish |
For the composer's other stores (messageComposer.state, textComposer.state, pollComposer.state, locationComposer.state, linkPreviewsManager.state), use useStateStore.
Messages, notifications, reminders and polls
| Hook | Reads | Reference |
|---|---|---|
useMessageContext, useMessageOperations, useIsMessageReadByOthers, useIsMessageDeliveredToOthers |
the message in context, its operations and its receipts | Message hooks |
useNotifications, useSystemNotifications, useNotificationApi, useNotificationTarget |
client.notifications.store, and the panel of the caller |
Notification hooks |
useMessageReminder |
client.reminders.state, for one message |
Reminder hooks |
usePollContext, usePollState, usePollAnswersPagination, usePollOptionVotesPagination |
the poll in context, its state, and its answers and votes | Poll hooks |
Used in: every page that renders state, and the Reference hook pages.