# 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](https://getstream.io/chat/docs/sdk/react-native/state/state-and-entities/reactive-state/). Getting the object comes first: [Providers and context hooks](https://getstream.io/chat/docs/sdk/react-native/ui/providers-and-context-hooks/) explains which client, channel, thread or composer a component gets.

Here is an unread badge for a channel list row:

```tsx
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>;
};
```

1. `channel` is the channel of the row the badge renders in: the `ChannelPreview` override gets it as a prop, and passes it on. [Providers and context hooks](https://getstream.io/chat/docs/sdk/react-native/ui/providers-and-context-hooks/) explains where each component gets its channel.
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.

## 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 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:

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

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

### 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.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/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. [Providers and context hooks](https://getstream.io/chat/docs/sdk/react-native/ui/providers-and-context-hooks/#contexts) 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](https://getstream.io/chat/docs/sdk/react-native/state/state-and-entities/events/)). The interval is a setting of the paginator: see [Message paginators](https://getstream.io/chat/docs/sdk/react-native/state/loading-data/message-paginators/).

## Hooks by state area

Every hook is documented in Reference, one page per state area:

- [State and entity hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/state-and-entity-hooks/)
- [Loading data hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/loading-data-hooks/)
- [Channel and thread hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/channel-and-thread-hooks/)
- [Channel preview hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/channel-preview-hooks/)
- [Channel details hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/channel-details-hooks/)
- [Connection hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/connection-hooks/)
- [Composer hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/composer-hooks/)
- [Message hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/message-hooks/)
- [Notification hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/notification-hooks/)
- [Reminder hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/reminder-hooks/)
- [Poll hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/poll-hooks/)
- [Location hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/location-hooks/)
- [AI hooks](https://getstream.io/chat/docs/sdk/react-native/reference/hooks/ai-hooks/)

Where no hook covers a store, read it with `useStateStore`.

---

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