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

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 type { ChannelStateData } from "stream-chat";
import { useChannel, useChatContext, useStateStore } from "stream-chat-react";

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 <span className="unread-badge">{unread}</span>;
};
  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() undefined the thread
useMessageComposerController() channel.messageComposer thread.messageComposer
useMessagePaginator() channel.messagePaginator thread.messagePaginator

Two more cases:

  • A composer you supply. Below a MessageComposerControllerProvider, useMessageComposerController() returns the composer that provider was given, for example the one editing a message inline.
  • A poll. Inside a poll in a message, usePollContext() returns { poll }. Read its state from poll.state.

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 type { MessagePaginatorState } from "stream-chat";
import { useMessagePaginator, useStateStore } from "stream-chat-react";

const itemsSelector = (state: MessagePaginatorState) => ({
  items: state.items,
});

export const LoadedMessageCount = () => {
  const paginator = useMessagePaginator();
  const { items } = useStateStore(paginator.state, itemsSelector);

  return <span>{items?.length ?? 0} messages loaded</span>;
};

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. 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 type { ChannelStateData } from "stream-chat";
import { useChannel, useStateStore } from "stream-chat-react";

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 <span>{member?.channel_role ?? "not a member"}</span>;
};

Pitfalls

Selector identity

An inline selector is a new function on every render, so the hook unsubscribes and subscribes again each time:

import type { ChannelStateData } from "stream-chat";
import { useChannel, useStateStore } from "stream-chat-react";

// Resubscribes on every render.
export const TypingCountInline = () => {
  const channel = useChannel();
  const { typing } = useStateStore(
    channel.state,
    (state: ChannelStateData) => ({
      typing: state.typing,
    }),
  );
  return <span>{Object.keys(typing).length}</span>;
};

// Subscribes once.
const typingSelector = (state: ChannelStateData) => ({ typing: state.typing });

export const TypingCount = () => {
  const channel = useChannel();
  const { typing } = useStateStore(channel.state, typingSelector);
  return <span>{Object.keys(typing).length}</span>;
};

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 two differ:

  • The one in stream-chat-react compares the keys of the previous selection with ===. The one in @stream-io/state-store compares the keys of the new selection with Object.is.
  • Only the one in stream-chat-react supports server rendering.

With selectors that follow the rules above, both see the same changes. Import the one from stream-chat-react.

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.
  • 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 → State.

Context hooks that throw

useChannel() throws outside a Channel or a Thread, and so do the hooks built on it, such as useMessageComposerController() and useChannelCapabilities(). useChatContext() throws outside Chat. A component that renders both inside and outside a channel, a channel-list row for example, reads useChannelInstanceContext() instead: outside a Channel it returns {}.

import { useChannelInstanceContext } from "stream-chat-react";

export const ChannelName = () => {
  const { channel } = useChannelInstanceContext();
  if (!channel) return null;
  return <span>{channel.cid}</span>;
};

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 useMessageComposerController(), 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 today. Each table links to its reference page. 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, channelManager, searchController, mutes
useChannel the channel of the nearest Channel or Thread
useChannelInstanceContext the Channel's channel, or {}
useThreadContext the Thread's thread, or undefined
useThreadListItemContext the thread of a ThreadList row
useIsUserMuted whether a user is in mutes

Loading data

Reference: Loading data hooks.

Hook Reads
useChannelListContext inside ChannelLists, the first paginator of client.channelManager
useChannelPaginatorState a ChannelPaginator's state: channels, has more, loading, error
useMessagePaginator the message list of the Thread, otherwise of the Channel
useThreadList nothing: it reloads client.threads and keeps it active while visible
useSearchContext, useSearchSourceResultsContext the search controller, and the source whose results are rendered
useChannelMembersSearch, useChannelMediaSearch, useChannelFilesSearch, usePinnedMessagesSearch channel details searches, from stream-chat-react/channel-detail

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
useChannelCapabilities channel.state ownCapabilities, as a Set
useChannelMembersState channel.state members
useChannelMembershipState channel.state membership: the current user's own membership
useChannelDisplayName the channel's display name
useChannelPreviewInfo the channel's display title and image
useCooldownRemaining channel.cooldownTimer.state: seconds left in slow mode
useIsCooldownActive channel.cooldownTimer.state: whether the user waits in slow mode
useLastReadData who has read each message

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
useNetworkConnectionState client.networkConnection.state: the device's network
useNetworkConnectionStateSelector a selection of client.networkConnection.state
useReportLostConnectionSystemNotification both, to publish the lost-connection notification, as Chat does

Writing messages

Reference: Composer hooks.

Hook Reads
useMessageComposerController the composer: supplied, the thread's or the channel's
useMessageComposerControllerContext the composer a MessageComposerControllerProvider supplies
useMessageComposerContext the MessageComposer component's own context
useAttachmentManagerState attachmentManager.state and the composer's attachment settings
useAttachmentsForPreview the attachments to show as previews
useAttachmentUploadState, useAttachmentsUploadState upload progress of one or several attachments
useCanCreatePoll whether pollComposer holds a poll that can be created
useMessageComposerHasSendableData, useMessageContentIsEmpty whether there is something to send
useMessageComposerCommands the commands available in the composer
useMessageComposerSubmitFn, useMessageComposerBindings the submit function, and the bindings MessageComposer puts in its context

For the composer's other stores (messageComposer.state, textComposer.state, pollComposer.state, locationComposer.state, linkPreviewsManager.state), use useStateStore.

Notifications, reminders, polls and location

Hook Reads Reference
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, usePollAnswerPagination, usePollOptionVotesPagination the poll in context, and its answers and votes Poll hooks
useLiveLocationSharingManager a live location sharing manager it creates Location hooks

For a poll's own state, use useStateStore(poll.state, selector) with the poll from usePollContext().

Used in: every page that renders state, and the Reference → Hooks pages.