Info:
This is release candidate documentation for Stream Chat React Native SDK v10. For the latest stable version, see the latest version (v9).

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>;
};
  1. useChannel() finds the channel: the Channel's, or the thread's channel inside a Thread.
  2. readSelector picks the read field from channel.state, which holds every member's read state.
  3. useStateStore subscribes to channel.state. When the channel's state changes, it runs the selector again and re-renders UnreadBadge only if read is a different value.
  4. The component works out the current user's count from read after 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 with usePollState(), or with useStateStore(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

  1. 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.
  2. 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.
  3. Derive values after selecting. Select the fields as they are and compute from them in the component, as UnreadBadge does. A selector that maps or filters builds a new array on every run, so the component re-renders on every change to the store.
  4. Import useStateStore from stream-chat-react-native (or stream-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. useStateStore compares 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.state is itself the channel's store. There is no channel.state.state.
  • client.notifications.store holds the notifications.
  • client.mutedUsersStore holds the users the current user has muted. useMutedUsers() reads it.
  • channel.messageReceiptsTracker.snapshotStore holds delivery and read receipts.
  • configState holds 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) and messageFocusSignal (the message to scroll to and highlight).
  • The thread list is client.threads.paginator.state. client.threads.state holds 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.