# Reading state in components

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

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](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/reactive-state/).

Here is an unread badge for the current channel:

```tsx
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.

```tsx
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:

```tsx
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:

```tsx
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](https://getstream.io/chat/docs/sdk/react-native/v10/reference/state/channel-and-channel-state/).

### 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](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/events/)). The interval is a setting of the paginator: see [Message paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/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](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/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](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/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](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/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](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/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](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/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](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/message-hooks/)           |
| `useNotifications`, `useSystemNotifications`, `useNotificationApi`, `useNotificationTarget`              | `client.notifications.store`, and the panel of the caller | [Notification hooks](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/notification-hooks/) |
| `useMessageReminder`                                                                                     | `client.reminders.state`, for one message                 | [Reminder hooks](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/reminder-hooks/)         |
| `usePollContext`, `usePollState`, `usePollAnswersPagination`, `usePollOptionVotesPagination`             | the poll in context, its state, and its answers and votes | [Poll hooks](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/poll-hooks/)                 |

Used in: every page that renders state, and the [Reference](https://getstream.io/chat/docs/sdk/react-native/v10/reference/hooks/state-and-entity-hooks/) hook pages.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/ui/reading-state-in-components/](https://getstream.io/chat/docs/sdk/react-native/v10/ui/reading-state-in-components/).