# SDK State Management

Most application state that drives the chat UI lives in the low-level client ([`StreamChat`](https://github.com/GetStream/stream-chat-js)) on client and channel instances. That state is **not reactive**. The React SDK makes it reactive by listening to client events and syncing changes into React state. Optimistic updates, when applicable, are local to the React SDK.

This is how the SDK state pipeline looks like behind the scenes:

<mermaid>

```text
sequenceDiagram
  box Stream Chat Client
    participant Client State
    participant WebSocket Client
  end

  box Stream React SDK
    participant UI Components
  end

  Client State->>WebSocket Client: attach listeners to keep the state up-to-date
  UI Components->>WebSocket Client: attach listeners to keep UI up-to-date
  WebSocket Client->>Client State: receive a WebSocket message and trigger listeners
  Client State->>Client State: update client state
  WebSocket Client->>UI Components: receive a WebSocket message and trigger listeners
  UI Components->>Client State: reach for updated client state
  UI Components->>UI Components: update UI
```

</mermaid>

## Active Channel & Channel State

[`ChatContext`](/chat/docs/sdk/react/v13/components/contexts/chat_context/) holds the active channel and the client instance passed to [`Chat`](/chat/docs/sdk/react/v13/components/core-components/chat/).
Before you can access the _reactive_ channel state you'll need to set the channel instance as active. The channel becomes active when:

- it’s selected in [`ChannelList`](/chat/docs/sdk/react/v13/components/core-components/channel_list/) (if you use the [default setup](/chat/docs/sdk/react/v13/basics/getting_started#your-first-app-with-stream-chat-react/), you already have `ChannelList`)
- it's passed to the `channel` prop of the [`Channel`](/chat/docs/sdk/react/v13/components/core-components/channel/) component

  ```tsx
  import { useState, useEffect } from "react";
  import { Channel, useChatContext } from "stream-chat-react";

  export const ChannelWrapper = ({
    channelId,
    channelType = "messaging",
    children,
  }) => {
    const [activeChannel, setActiveChannel] = useState(undefined);
    const { client } = useChatContext();

    useEffect(() => {
      if (!channelId) return;

      const channel = client.channel(channelType, channelId);

      setActiveChannel(channel);
    }, [channelId, channelType]);

    return <Channel channel={activeChannel}>{children}</Channel>;
  };
  ```

- it's set as active by calling the [`setActiveChannel`](/chat/docs/sdk/react/v13/components/contexts/chat_context#setactivechannel/) function coming from the [`ChatContext`](/chat/docs/sdk/react/v13/components/contexts/chat_context/) (this function is used by [`ChannelList`](/chat/docs/sdk/react/v13/components/core-components/channel_list/) behind the scenes)

  ```tsx
  import { useEffect } from "react";
  import {
    useCreateChatClient,
    useChatContext,
    Chat,
    Channel,
  } from "stream-chat-react";

  const ActiveChannelSetter = ({ channelId, channelType }) => {
    const { client, setActiveChannel } = useChatContext();

    useEffect(() => {
      const channel = client.channel(channelType, channelId);
      setActiveChannel(channel);
    }, [channelType, channelId]);

    return null;
  };

  const App = () => {
    const client = useCreateChatClient(userData);

    if (!client) return <div>Loading...</div>;

    return (
      <Chat client={client}>
        <ActiveChannelSetter channelId="random" channelType="messaging" />
        <Channel>{"...other components..."}</Channel>
      </Chat>
    );
  };
  ```

## Best Practices

- Treat client and channel state as non-reactive; rely on SDK contexts.
- Choose one active-channel strategy (`ChannelList`, `channel` prop, or `setActiveChannel`), not multiple.
- Keep selectors stable and memoized to avoid re-subscriptions.
- Read only the state slice you need for performance.
- Avoid heavy computations inside selectors or render paths.

<admonition type="note">

You can use either `channel` prop on the [`Channel`](/chat/docs/sdk/react/v13/components/core-components/channel/) component or [`setActiveChannel`](/chat/docs/sdk/react/v13/components/contexts/chat_context#setactivechannel/) function. You cannot use both at the same time.

</admonition>

Currently active [channel state](https://github.com/GetStream/stream-chat-react/blob/master/src/context/ChannelStateContext.tsx#L36-L56) and channel instance can be accessed through the [`ChannelStateContext`](/chat/docs/sdk/react/v13/components/contexts/channel_state_context/) with the help of the [`useChannelStateContext`](/chat/docs/sdk/react/v13/components/contexts/channel_state_context#basic-usage/) hook - meaning any component which is either direct or indirect child of the [`Channel`](/chat/docs/sdk/react/v13/components/core-components/channel/) component can access such state.

The example below shows how to access `members` and `channel` from channel state:

```tsx
import { useEffect } from "react";
import { Channel, useChannelStateContext } from "stream-chat-react";

const MembersCount = () => {
  const { members, channel } = useChannelStateContext();

  useEffect(() => {
    console.log(`Currently active channel changed, channelId: ${channel.id}`);
  }, [channel]);

  return <div>{Object.keys(members).length}</div>;
};

const ChannelWrapper = () => (
  <Channel>
    <MembersCount />
  </Channel>
);
```

## Channel List State

[`ChannelList`](/chat/docs/sdk/react/v13/components/core-components/channel_list/) is a standalone component that manages the channel list. Access loaded channels via [`ChannelListContext`](/chat/docs/sdk/react/v13/components/contexts/channel_list_context/) and the `useChannelListContext` hook. Any child of `ChannelList` (for example, [Channel Preview](/chat/docs/sdk/react/v13/components/utility-components/channel_preview_ui/)) can access this state.

```tsx
import { ChannelList, ChannelPreviewMessenger } from "stream-chat-react";
import type { ChannelListProps } from "stream-chat-react";

const CustomPreviewUI = (props) => {
  const { channels } = useChannelListContext();

  return <ChannelPreviewMessenger {...props} />;
};

export const CustomChannelList = (props: ChannelListProps) => {
  return <ChannelList Preview={CustomPreviewUI} {...props} />;
};
```

## Thread and ThreadManager

With the new [threads feature](/chat/docs/sdk/react/v12/release-guides/upgrade-to-v12#introducing-threads-20/), state management moved to a subscribable POJO with selector-based access to improve DX when rendering data from `StreamChat`.

<admonition type="note">

This change is currently only available within `StreamChat.threads` but will be reused across the whole SDK later on.

</admonition>

### Why POJO (State Object)

The SDK exposes a lot of data, and different apps need different slices of it. That data now lives in a state object, and selectors let you pick exactly what you need without extra overhead.

### What are Selectors

Selectors run whenever the state object changes and should return only the slice needed by a component. Don’t do heavy computation or create new arrays/objects on each run; use pre-built hooks with computed values or build memoized selectors as needed.

#### Rules of Selectors

1. Selectors should return a named object.

```ts
const selector = (nextValue: ThreadManagerState) => ({
  unreadThreadsCount: nextValue.unreadThreadsCount,
  active: nextValue.active,
  lastConnectionDownAt: nextvalue.lastConnectionDownAt,
});
```

2. Selectors should live outside component scope or be memoized if they depend on external data (for example, `userId` for `read`). Unstable selectors cause unnecessary unsubscribe/resubscribe cycles and hurt performance.

```tsx
// ❌ not okay
const Component1 = () => {
  const { latestReply } = useThreadState((nextValue: ThreadState) => ({
    latestReply: nextValue.latestReplies.at(-1),
  }));

  return <div>{latestReply.text}</div>;
};

// ✅ okay
const selector = (nextValue: ThreadState) => ({
  latestReply: nextValue.latestReplies.at(-1),
});

const Component2 = () => {
  const { latestReply } = useThreadState(selector);

  return <div>{latestReply.text}</div>;
};

// ✅ also okay
const Component3 = ({ userId }: { userId: string }) => {
  const selector = useCallback(
    (nextValue: ThreadState) => ({
      unreadMessagesCount: nextValue.read[userId].unread_messages,
    }),
    [userId],
  );

  const { unreadMessagesCount } = useThreadState(selector);

  return <div>{unreadMessagesCount}</div>;
};
```

3. Break your components down to the smallest reasonable parts that each take care of the apropriate piece of state if it makes sense to do so.

### Accessing Reactive State

Our SDK currently allows to access two of these state structures - in [`Thread`](https://github.com/GetStream/stream-chat-js/blob/master/src/thread.ts) and [`ThreadManager`](https://github.com/GetStream/stream-chat-js/blob/master/src/thread_manager.ts) instances under `state` property.

#### Vanilla

```ts
import { StreamChat } from "stream-chat";

const client = new StreamChat(/*...*/);

// calls console.log with the whole state object whenever it changes
client.threads.state.subscribe(console.log);

let latestThreads;
client.threads.state.subscribeWithSelector(
  // called each time theres a change in the state object
  (nextValue) => ({ threads: nextValue.threads }),
  // called only when threads change (selected value)
  ({ threads }) => {
    latestThreads = threads;
  },
);

// returns lastest state object
const state = client.threads.state.getLatestValue();

const [thread] = latestThreads;

// thread instances come with the same functionality
thread?.state.subscribe(/*...*/);
thread?.state.subscribeWithSelector(/*...*/);
thread?.state.getLatestValue(/*...*/);
```

#### useStateStore Hook

For ease of use, the React SDK provides the `useStateStore` hook, which wraps the `StateStore.subscribeWithSelector` API for React applications.

```tsx
import { useStateStore } from "stream-chat-react";
import type { ThreadManagerState } from "stream-chat";

const selector = (nextValue: ThreadManagerState) => ({
  threads: nextValue.threads,
});

const CustomThreadList = () => {
  const { client } = useChatContext();
  const { threads } = useStateStore(client.threads.state, selector);

  return (
    <ul>
      {threads.map((thread) => (
        <li key={thread.id}>{thread.id}</li>
      ))}
    </ul>
  );
};
```

#### useThreadState and useThreadManagerState

Both of these hooks use `useStateStore` under the hood but access their respective states through specific contexts: for `ThreadManagerState` it's `ChatContext` (accessing `client.threads.state`), and for `ThreadState` it's `ThreadListItemContext` first and `ThreadContext` second, meaning that the former is prioritized. While these hooks make it slightly easier for integrators to reach reactive state

```ts
// memoized or living outside component's scope
const threadStateSelector = (nextValue: ThreadState) => ({
  replyCount: nextValue.replyCount,
});
const threadManagerStateSelector = (nextValue: ThreadState) => ({
  threadsCount: nextValue.threads.length,
});

const MyComponent = () => {
  const { replyCount } = useThreadState(threadStateSelector);
  const { threadsCount } = useThreadManagerState(threadManagerStateSelector);

  return null;
};
```

## Conclusion

This guide covers the biggest and most important state stores, see other React stateful contexts exported by our SDK for more information.

Mentioned in this article:

- [`ChannelStateContext`](/chat/docs/sdk/react/v13/components/contexts/channel_state_context/)
- [`ChatContext`](/chat/docs/sdk/react/v13/components/contexts/chat_context/)
- [`ChannelListContext`](/chat/docs/sdk/react/v13/components/contexts/channel_list_context/)

Other data/action providers:

- [`ComponentContext`](/chat/docs/sdk/react/v13/components/contexts/component_context/)
- [`ChannelActionContext`](/chat/docs/sdk/react/v13/components/contexts/channel_action_context/)
- [`MessageContext`](/chat/docs/sdk/react/v13/components/contexts/message_context/)
- [`MessageInputContext`](/chat/docs/sdk/react/v13/components/contexts/message_input_context/)
- [`MessageListContext`](/chat/docs/sdk/react/v13/components/contexts/message_list_context/)


---

This page was last updated at 2026-04-21T09:53:41.022Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v13/guides/sdk-state-management/](https://getstream.io/chat/docs/sdk/react/v13/guides/sdk-state-management/).