# Upgrade to v15

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

This page lists what changed between v14 and v15, and what to change in your app. Each change links to the v15 page that explains how the new version works.

v15 needs `stream-chat` 10, version 10.0.0-rc.20 or later. Upgrade both packages together, as [Installation](https://getstream.io/chat/docs/sdk/react/v15/getting-started/installation/) describes. Many of the changes below come from `stream-chat` 10. This page covers the ones a React app meets. The complete list of client changes is [stream-chat v9 to v10 breaking changes](https://github.com/GetStream/stream-chat-js/blob/release-v10/ai-docs/v10-breaking-changes.md) in the `stream-chat` repository.

## Quick migration checklist

1. **Upgrade both packages:** `npm install stream-chat@rc stream-chat-react@rc`.
2. **Move client options under `client`.** `useCreateChatClient` takes `config` instead of `options`. See [Creating the client](#creating-the-client).
3. **Update how you read data.** Your custom fields are under `custom`, so a channel's name is `channel.data?.custom?.name`. Server timestamps are nanosecond numbers, and sorts are arrays. See [Data shapes](#data-shapes).
4. **Rewrite every `ChannelList`.** Create a `ChannelPaginator`, register it on `client.channelManager`, and pass it as the `paginator` prop. See [Channel lists](#channel-lists).
5. **Watch a channel before you pass it to `Channel`.** `Channel` no longer queries. Use `Channel.watch()`. See [Channel no longer queries the channel](#channel-no-longer-queries-the-channel).
6. **Remove `Window`, and pass `thread` to `Thread`.** Render the thread's header, list and composer as its children. See [Window is gone](#window-is-gone) and [Thread takes a thread and renders its children](#thread-takes-a-thread-and-renders-its-children).
7. **Replace `useChannelStateContext` and `useChannelActionContext`.** Both contexts are removed. Read the channel's stores with `useChannel()` and `useStateStore`, and call methods on the channel. See [Removed contexts](#removed-contexts).
8. **Replace `setActiveChannel`.** Open a channel with `useWorkspaceNavigation().openChannel(channel)`. See [ChatContext has no active channel](#chatcontext-has-no-active-channel).
9. **Import `ChatView` from `stream-chat-react/slot-layout`,** and replace `ChatView.Channels`, `ChatView.Threads` and `ChatView.ThreadAdapter` with views and slots. See [ChatView is a slot layout](#chatview-is-a-slot-layout).
10. **Move the `do*Request` props to `client.config`.** See [Channel props that moved](#channel-props-that-moved).
11. **Rename your translation keys.** See [Localization](#localization).
12. **Rename `Message` overrides to `MessageUI`, and `disableDateSeparator` to `withDateSeparator`.** See [Components](#components).
13. **Replace `connection.changed` listeners.** See [Connection status is two facts now](#connection-status-is-two-facts-now).

## What's new

- [How the SDK is built](https://getstream.io/chat/docs/sdk/react/v15/getting-started/how-the-sdk-is-built/): the state layer in `stream-chat` and the UI layer in `stream-chat-react`, followed through one sent message.
- [Configuration](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/): `client.config`, the one place to configure the client and the objects it creates.
- [Channel list paginators](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/channel-list-paginators/) and [Channel manager and ChannelPaginator](https://getstream.io/chat/docs/sdk/react/v15/reference/state/channel-manager-and-channelpaginator/): channel lists as objects, with `ChannelManager` keeping several lists current.
- [Message paginators](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/message-paginators/) and [Message paginator](https://getstream.io/chat/docs/sdk/react/v15/reference/state/message-paginator/): message intervals, jumping to a message, and unread tracking.
- [Message operations](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/message-operations/): the optimistic send, retry, update and delete, and the request handlers that customize them.
- [Connection](https://getstream.io/chat/docs/sdk/react/v15/state/connection/): the device's network and the WebSocket as two separate reactive services.
- [Reading state in components](https://getstream.io/chat/docs/sdk/react/v15/ui/reading-state-in-components/): `useStateStore`, which hook reads which store, and the selector rules.
- [Navigation](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/navigation/) and the [Workspace navigation API](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/workspace-navigation-api/): how components open channels and threads.
- [Workspace with ChatView](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/), [ChatView components](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/chatview-components/) and [Slot layout](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/slot-layout/): the multi-panel workspace.
- [Slot geometry](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/slot-geometry/): detecting which panels are covered.

## Localization

Two changes affect anyone who customized translations, and a third affects code that calls the `Streami18n` instance directly. If you use the SDK in English and never touched i18n, there is nothing to do.

The v15 behaviour is on [Localization](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/localization/). This section is the migration.

### English is the only bundled language

The `de`, `es`, `fr`, `hi`, `it`, `ja`, `ko`, `nl`, `pt`, `ru` and `tr` dictionaries are removed, along with their `dayjs` locale data. The `deTranslations` to `trTranslations` exports are gone with them, and so is `enTranslations`.

Those dictionaries were imported statically and copied into `Streami18n` when it was created, so they shipped even in apps that only rendered English. Removing them, together with the key change below, takes about 112 KB gzipped out of the bundle.

If you rendered one of them, supply it yourself. The last published files are in the SDK's git history:

```bash
git show v14.13.2:src/i18n/de.json > de.json
```

Its keys are the old natural-language ones, so rename them (see below) before you register it. You also need the `dayjs` locale and the calendar formats the SDK used to bundle for each language. [Date and time for a new language](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/localization/#date-and-time-for-a-new-language) shows both.

`SupportedTranslations` and `isLanguageSupported` are removed. `language`, and the `language` option of `Streami18n`, are plain `string`s, because the languages are whatever you register.

`Chat`'s `defaultLanguage` prop is removed. In v14, when you passed no `i18nInstance`, `Chat` created one in the user's language, else the browser's language if the SDK bundled it, else `defaultLanguage`. In v15 the instance `Chat` creates is English. Delete the prop. For a non-English UI, register a dictionary and set `language` on your `Streami18n` instance. For translated messages, set `language` on the user in `client.connectUser()`.

`userLanguage`, the language the SDK reads auto-translated message text in, no longer falls back to the browser's language either. A registered German dictionary says nothing about whether the API produces `message.i18n.de_text`: without `client.user.language` the API doesn't translate. `userLanguage` follows `client.user.language`, or `en` when that's unset, and it follows changes made after the user connects.

### Translation keys are namespaced identifiers

A key used to be the English copy itself. It's now a stable dotted identifier, and each call passes the English copy as `i18next`'s `defaultValue`. The v14 key `aria/Send` became:

```ts
t("messageComposer.sendButton.send.ariaLabel", "Send");
```

The `aria/` prefix is gone. What the text is for moved into the last segment of the key, `ariaLabel` here.

>
> **Warning:** Renaming your keys isn't optional, and a missed one fails quietly. An old key never matches, so your translation stops applying and the SDK's English copy renders instead, with no error and nothing in the console.
>

The table that maps v14 keys to v15 keys, 621 rows, is [`ai-docs/i18n-v15-key-map.json`](https://github.com/GetStream/stream-chat-react/blob/master/ai-docs/i18n-v15-key-map.json) in the SDK repository. It covers most v14 keys. A v14 key with no row is mostly copy that v15 no longer renders. If its copy still shows in your UI, find the v15 key by its English text in the `TranslationCatalog` type.

A few rows of the table:

```json
{
  "keys": {
    "Cancel": { "key": "common.cancel.label", "prose": true, "shared": true },
    "aria/Send": {
      "key": "messageComposer.sendButton.send.ariaLabel",
      "prose": true
    },
    "{{ count }} members": {
      "key": "channelDetail.channelMembersView.members.title",
      "prose": true,
      "plural": true
    },
    "giphy-command-args": { "key": "command.giphy.args", "prose": true },
    "language/de": { "key": "language.de", "prose": true },
    "timestamp/MessageTimestamp": {
      "key": "timestamp.MessageTimestamp",
      "prose": false
    }
  }
}
```

A German dictionary in v14:

```ts
i18n.registerTranslation("de", {
  Cancel: "Abbrechen",
  "aria/Send": "Senden",
  "{{ count }} members_one": "{{ count }} Mitglied",
  "{{ count }} members_other": "{{ count }} Mitglieder",
});
```

The same dictionary in v15:

```ts
import { Streami18n } from "stream-chat-react";
import type { TranslationDictionary } from "stream-chat-react";

const i18n = new Streami18n();

const de: TranslationDictionary = {
  "common.cancel.label": "Abbrechen",
  "messageComposer.sendButton.send.ariaLabel": "Senden",
  "channelDetail.channelMembersView.members.title_one": "{{ count }} Mitglied",
  "channelDetail.channelMembersView.members.title_other":
    "{{ count }} Mitglieder",
};

i18n.registerTranslation("de", de);
```

### Let TypeScript catch the ones you miss

Typing the dictionary as `TranslationDictionary` turns the quiet failure into a compile error, which is the fastest way through the rename:

```ts
const de: TranslationDictionary = {
  Cancel: "Abbrechen", // TS2353: 'Cancel' does not exist in type ...
};
```

`Streami18n.registerTranslation()` and `translationsForLanguage` already take that type, so a dictionary written inline is checked without the annotation. [Typed dictionaries](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/localization/#typed-dictionaries) covers the rest, including how to learn that a later release added a key.

### Keys whose value is not copy

The `timestamp/*`, `duration/*` and `translationBuilderTopic/*` groups became `timestamp.*`, `duration.*` and `translationBuilderTopic.*`. Their values are `dayjs` and `i18next` expressions, not text, and they no longer live in a locale file. The SDK ships them as defaults that every language you register is layered over, so you list one only to change it. Four `timestamp/relative*` keys were text, and became `relativeTime.today`, `relativeTime.yesterday`, `relativeTime.daysAgo` and `relativeTime.weeksAgo`.

Four `timestamp.*` keys put English day words, such as "Today", into their `calendarFormats` argument, and need an override to translate them. See [Keys that are not copy](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/localization/#keys-that-are-not-copy).

### There is no en.json

The SDK no longer checks in an English locale file. The copy is written at each `t()` call, so a committed catalog would be a second copy that could go stale.

To get a file for a translator or a translation service, generate it from a clone of the SDK with `yarn i18n:export`. To see a key's English copy in your editor, index the type-only `TranslationCatalog` export.

### The translation runtime

`Streami18n` is built on the shared `@stream-io/i18n` package. You still import it from `stream-chat-react`, and its constructor options are the same. Code that calls the instance directly needs these changes:

| v14                                                    | v15                                                                                                                      |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `await i18n.getTranslators()`                          | `await i18n.init()`, which resolves to the instance's state, `t` and `tDateTimeParser` included, and rejects if it fails |
| assigning `i18n.t`                                     | `i18n.overrideTFunction(t)`                                                                                              |
| `const t = await i18n.setLanguage("de")`               | `setLanguage()` resolves to nothing. Read `t` from the instance's state                                                  |
| `i18n.registerSetLanguageCallback(callback)`           | subscribe to `i18n.state`                                                                                                |
| `i18n.getAvailableLanguages()`                         | `i18n.registeredLanguages`, a read-only `Set`                                                                            |
| `i18n.getTranslations()`                               | removed. The English copy is in the `TranslationCatalog` type                                                            |
| `i18n.geti18Instance()`                                | removed                                                                                                                  |
| `translators` from `useChat({ client, i18nInstance })` | `useStreami18n({ client, i18nInstance })`                                                                                |

The current `t` is in the instance's state:

```ts
await i18n.setLanguage("de");
const { t } = i18n.state.getLatestValue();
```

`@stream-io/i18n` depends on `i18next` and `dayjs`. If you declared them only for this SDK, you can remove them. If you keep `dayjs`, use the same version range, so the locale you import registers on the copy that formats the dates.

## Creating the client

`stream-chat` 10 builds the client from one configuration tree, the same tree `client.config.set()` takes. Most of the options v9 took at the top level now go under `client`. `useCreateChatClient` passes that tree as `config`, which replaces its `options` prop.

In v14:

```tsx
const client = useCreateChatClient({
  apiKey,
  options: { baseURL: "https://chat.example.com", timeout: 6000 },
  tokenOrProvider: userToken,
  userData: { id: userId },
});
```

In v15:

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

export const App = () => {
  const client = useCreateChatClient({
    apiKey,
    config: {
      client: {
        axiosRequestConfig: { timeout: 6000 },
        baseURL: "https://chat.example.com",
      },
    },
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  if (!client) return null;

  return <Chat client={client}>{/* your app */}</Chat>;
};
```

A client you create yourself takes the same tree: `new StreamChat(apiKey, { client: { baseURL } })`. The `secret` argument is gone, because server-side code belongs in `@stream-io/node-sdk`. In plain JavaScript, a v9 options object still runs, but the client ignores its keys and logs a warning for each.

Where the common v9 options went:

| v9 option                                                    | v10 setting                                                           |
| ------------------------------------------------------------ | --------------------------------------------------------------------- |
| `timeout`, `headers` and the other top-level `axios` options | `client.axiosRequestConfig`                                           |
| `baseURL`                                                    | `client.baseURL`                                                      |
| `enableWSFallback`                                           | `client.wsConnection.enableWSFallback`                                |
| `wsUrlParams`                                                | `client.wsConnection.urlParams`                                       |
| `recoverStateOnReconnect`                                    | `client.connectionRecovery.enabled`                                   |
| `persistUserOnConnectionFailure`                             | `client.persistUserOnConnectionFailure`                               |
| `isLocalUnreadCountEnabled`                                  | `channel.readEvents.localUnreadCountEnabled`                          |
| `logger`                                                     | removed. Configure logging with `chatLoggerSystem` from `stream-chat` |
| `device`                                                     | removed. Call `client.createDevice()` after connecting                |
| `warmUp`, `enableInsights`, `disableCache`                   | removed                                                               |

[Configuration](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/) explains the tree, which settings can change after the client is created, and setup functions.

## Data shapes

`stream-chat` 10 types its data from the API's specification. Three changes reach almost every app.

### Custom fields are under custom

Your own fields on a message, a channel, a user or another object are no longer spread at the top level. They're in the object's `custom` field:

| v9                                   | v10                                                      |
| ------------------------------------ | -------------------------------------------------------- |
| `message.priority`                   | `message.custom.priority`                                |
| `channel.data?.name`                 | `channel.data?.custom?.name`                             |
| `channel.data?.image`                | `channel.data?.custom?.image`                            |
| `Channel.sendMessage({ text, ... })` | `Channel.sendMessage({ message: { text, custom: {} } })` |

The channel's name and image are the reads most apps have to fix. Filtering is the exception: filter a channel's name as `name`, and other custom fields as `"custom.<field>"`. To type your fields, extend the `Custom*Data` interfaces, as [Custom data types](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/data-conventions/#custom-data-types) shows.

### Timestamps are nanosecond numbers

Every date the server sends, such as `created_at`, is a `TimestampNS`: Unix time in nanoseconds, as a `number`. In v9 it was an ISO string, and messages carried `Date`s.

- Compare and sort timestamps as numbers: `a.created_at < b.created_at`.
- Convert where a timestamp meets a `Date`, a date library or `Date.now()`, with `nsToDate()`, `convertTimestampToDate()` or `nsToMs()` from `stream-chat`.
- `new Date(message.created_at)` and `dayjs(message.created_at)` read nanoseconds as milliseconds. The first fails to compile where you use the result as a `Date`. The second compiles and renders "Invalid Date".

The React props that carry a timestamp changed with it. `MessageList`'s `headerPosition` is a `TimestampNS`, and so are `lastRead` in `processMessages` and `lastReadDate` in the `VirtualizedMessageList` render props. Components that render a date, such as `DateSeparator`, still take a `Date`. [Timestamps](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/data-conventions/#timestamps) lists the helpers.

### Sorts are arrays

Every sort is an array of `{ field, direction }` objects, typed `SortParamRequest[]`. The object form and the per-endpoint sort types, such as `ChannelSort`, are removed:

```ts
import type { SortParamRequest } from "stream-chat";

// v9: { pinned_at: -1, last_message_at: -1 }
const sort: SortParamRequest[] = [
  { direction: -1, field: "pinned_at" },
  { direction: -1, field: "last_message_at" },
];
```

Keep the order of the fields: the first one sorts first.

### Other client changes

These come up less often. The [complete list](https://github.com/GetStream/stream-chat-js/blob/release-v10/ai-docs/v10-breaking-changes.md) has each one in detail.

- **Raw requests and the optimistic layer.** `client.queryChannels()`, `Channel.sendMessage()`, `Channel.sendReaction()`, `client.updateMessage()` and `client.deleteMessage()` are plain requests that change no local state. The v9 behaviour of `queryChannels` is `client.queryChannelsAndHydrate()`. Sending, editing and deleting with an optimistic update goes through the composer and `channel.messageOperations`; see [Message operations](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/message-operations/).
- **Channels the client holds.** `client.activeChannels`, `client.getChannelById()` and `client.getChannelByMembers()` are removed. Use `client.channelManager.get(cid)`, `client.channelManager.values()`, and `client.channel()`. See [Shared entities](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/shared-entities/).
- **Channel state.** `channel.state` is a store, the message list moved to `channel.messagePaginator`, and pinned messages to `channel.pinnedMessagesPaginator`. See [Channel state is one store](#channel-state-is-one-store).
- **`Channel.watch()`.** It resolves with the channel instance to use, not the server's response: read the data from the channel, such as `channel.data`, `channel.state.members` and `channel.messagePaginator.items`. It sends a request only when the channel isn't loaded yet, or when you pass options it wasn't watched with, so it no longer refreshes a watched channel: call `Channel.reload()` for that.
- **`Channel.query()`.** Its second argument, the message set to add the messages to, is removed, and the messages it returns aren't added to `channel.messagePaginator`. Open a channel with `Channel.watch()`, and load more messages with `MessagePaginator.toTail()`, `MessagePaginator.toHead()` or `MessagePaginator.jumpToMessage()`.
- **Threads.** A thread's replies are in `thread.messagePaginator`, and the thread list is `client.threads.paginator`. Get a thread with `client.threads.ensure({ channel, parentMessage })`, not `new Thread()`. `Thread.activate()` returns the function that ends the activation, and `Thread.deactivate()` is removed.
- **Marking read.** `Channel.markRead()` sends the request directly. The v9 `Channel.markRead()` path, through the delivery reporter, is `Channel.markReadViaReporter()`, which also calls your `markReadRequest` handler if you register one.
- **Events.** `Event` is a union of event types, `EventTypes` is `EventType`, and custom event fields are under `event.custom`.
- **Errors.** API errors are `StreamAPIError`, which replaces `ErrorFromResponse`.
- **`StateStore`.** `stream-chat` no longer exports it. Import it from `@stream-io/state-store`, and keep one copy of that package installed.
- **Link previews.** The composer loads link previews by default where the channel type enriches URLs. To turn them off: `client.config.set({ messageComposer: { linkPreviews: { enabled: false } } })`.
- **Node.js.** `stream-chat` needs Node.js 22.18 or later, and no longer bundles a WebSocket for Node.

## Renamed and deprecated APIs

The left column keeps working in v15. These are deprecated aliases, not removed APIs, but new code should use the right column.

| Deprecated                                   | Use                                                     |
| -------------------------------------------- | ------------------------------------------------------- |
| `client.setMessageComposerSetupFunction(fn)` | `client.config.setSetupFunction("messageComposer", fn)` |
| `Thread.markAsRead()`                        | `Thread.markRead()`                                     |
| `client.userID`, `client.clientID`           | `client.userId`, `client.clientId`                      |

The paginators' old method names are removed, not deprecated. A call to them doesn't compile:

| Removed                     | Use                           |
| --------------------------- | ----------------------------- |
| `paginator.next()`          | `paginator.toTail()`          |
| `paginator.prev()`          | `paginator.toHead()`          |
| `paginator.nextDebounced()` | `paginator.toTailDebounced()` |
| `paginator.prevDebounced()` | `paginator.toHeadDebounced()` |
| `paginator.hasNext`         | `paginator.hasMoreTail`       |
| `paginator.hasPrev`         | `paginator.hasMoreHead`       |
| `paginator.headItems`       | `paginator.anchoredHeadItems` |
| `paginator.headmostItem`    | `paginator.anchoredHeadItem`  |

A paginator's state has no `hasNext` and `hasPrev` either, so a selector reading them gets `undefined`: select `hasMoreTail` and `hasMoreHead`. In the same way, a cursor's `next` and `prev` are `tailward` and `headward`, and the `hasNext` and `hasPrev` of `useChannelPaginatorState()` are `hasMoreTail` and `hasMoreHead`.

In a message list, the tail is where older messages load, and the head is the newest message. [Head and tail](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/how-pagination-works/#head-and-tail) explains both ends.

The paginator classes moved inside `stream-chat` to `pagination/paginators/`. Import them from `stream-chat`, never from a path inside `stream-chat/dist`.

## Connection status is two facts now

Until v15 the SDK had one notion of "online": whether its WebSocket was up. It inferred the device's network from that socket, which is wrong both ways. A socket dies on working Wi-Fi when the server closes it or a token expires, and a device can get its network back seconds before the socket. `stream-chat` 10 splits them into `client.networkConnection`, whether the device has a network, and `client.wsConnection`, whether the socket is up.

You're affected if you listen for connectivity, render your own offline UI, or read the socket directly:

| v14                                         | v15                                                                                                                                       |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `connection.changed` event                  | `client.wsConnection.state` or `client.networkConnection.state`. In components, `useWSConnectionState()` or `useNetworkConnectionState()` |
| `transport.changed` event                   | the local `connection.fallback_activated` event                                                                                           |
| `client.wsConnection`, the socket or `null` | `client.wsConnection` always exists. It reports `isHealthy` and holds the socket's settings                                               |
| `client.defaultWSTimeout`                   | `client.wsConnection.config.connectTimeoutMs`, set as `client.wsConnection.connectTimeoutMs` in the configuration                         |
| `client.wsFallback`                         | removed                                                                                                                                   |
| `ThreadManagerState.lastConnectionDropAt`   | `lastUnhealthyAt` in `client.wsConnection.state`                                                                                          |

Two of these fail without a compile error, so check for them by hand:

- **`connection.changed` handlers.** The event no longer exists, so a listener for it doesn't compile. Subscribe to the store you meant instead. If the handler showed a "connection lost" banner, it relied on the event holding a drop back for five seconds, so a short flap didn't flash the banner. The stores publish every change at once, so wait `client.wsConnection.config.offlineNotificationDisplayDelayMs` yourself, and cancel if the socket comes back.
- **Options passed as a separate object.** TypeScript rejects the old option names when you pass the options inline. An options object built elsewhere gets past that check, and the client ignores it.

The two stores name their fields differently. The socket's `isHealthy` is always a boolean. The network's `isOnline` is `undefined` until a reporter has reported, so test it with `=== false`.

On React Native, the browser events the built-in network reporter uses don't exist. The client installs a stand-in that repeats what the socket reports, so nothing breaks, but it adds no information. Install a reporter built on the platform's network API for a real signal. See [Device network](https://getstream.io/chat/docs/sdk/react/v15/state/connection/#device-network).

Watching changed too. A request that watches a channel or subscribes to presence waits for the WebSocket's connection id, which the server ties the subscription to. Before, it went out without one, got a `200`, and was registered nowhere. It waits rather than fails, so it rides out a reconnect. It fails when no socket is open or opening, or when the socket is closed while it waits. A query that asks for neither isn't held. See ["No connection id is available"](https://getstream.io/chat/docs/sdk/react/v15/troubleshooting/#no-connection-id-is-available).

Recovery after a reconnect changed too. `stream-chat` 9 re-queried the loaded channels in one request. `stream-chat` 10 queries again the channels each registered channel list has loaded, up to 150, page by page, and removes those no page returned. It also reloads each active channel and thread, and then dispatches `connection.recovered`. See [Connection recovery](https://getstream.io/chat/docs/sdk/react/v15/state/connection/#connection-recovery).

## Channels

### Channel props that moved

`Channel` has one prop: the channel. Everything else that used to hang off it now belongs to the component or the layer that uses it:

| v14 prop on `Channel`                                                                                      | v15                                                                                                                                                                                                                   |
| ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `giphyVersion`, `imageAttachmentSizeHandler`, `videoAttachmentSizeHandler`, `shouldGenerateVideoThumbnail` | props of [`Attachment`](https://getstream.io/chat/docs/sdk/react/v15/component-reference/message/attachments/). Register an `Attachment` wrapper through `WithComponents`                                                                 |
| `onMentionsClick`, `onMentionsHover`                                                                       | props of [`MessageList`](https://getstream.io/chat/docs/sdk/react/v15/component-reference/message-list/messagelist/)                                                                                                                      |
| `doSendMessageRequest`, `doUpdateMessageRequest`, `doDeleteMessageRequest`, `doMarkReadRequest`            | request handlers in [`client.config`](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/configuration/). See [Request handlers](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/message-operations/#request-handlers) |
| `initializeOnMount`, `channelQueryOptions`                                                                 | removed. See [Channel no longer queries the channel](#channel-no-longer-queries-the-channel)                                                                                                                          |
| `activeUnreadHandler`                                                                                      | removed. See [Channel no longer writes document.title](#channel-no-longer-writes-documenttitle)                                                                                                                       |
| `EmptyPlaceholder`                                                                                         | removed. See [ChannelProps.channel is required](#channelpropschannel-is-required-emptyplaceholder-is-gone)                                                                                                            |
| `allowConcurrentAudioPlayback`                                                                             | removed. See [Audio playback](#audio-playback)                                                                                                                                                                        |
| `markReadOnMount`                                                                                          | removed. The message list marks the channel read when the user is at the newest message                                                                                                                               |
| `skipMessageDataMemoization`                                                                               | removed. Messages come from the channel's message list store                                                                                                                                                          |

The request handlers change shape as well as place. The message handlers take a params object instead of positional arguments, plus the default request as a second argument. They resolve to `{ message }`. They're registered once per client, not per mounted `Channel`, so to treat channels differently, branch inside the handler.

In v14:

```tsx
<Channel channel={channel} doSendMessageRequest={mySend}>
  {/* ... */}
</Channel>
```

In v15:

```ts
client.config.set({
  channel: {
    requestHandlers: {
      sendMessageRequest: async ({ message, options }) => ({
        message: await mySend(message, options),
      }),
    },
  },
});
```

Thread replies go through the same `channel` handlers. The `thread` key takes only `markReadRequest`, which runs when a thread is marked read.

`useChannelEditMessageHandler` is removed with the props. An `updateMessageRequest` handler covers edits.

### Channel no longer queries the channel

`Channel` used to call `Channel.watch()` when it mounted a channel that wasn't loaded, and `initializeOnMount` turned that off. Both are gone, along with `channelQueryOptions` and the `getChannel` helper. Whoever supplies the channel loads it:

```tsx
import { useEffect, useState } from "react";
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  ChannelPlaceholder,
  MessageComposer,
  MessageList,
  useChatContext,
} from "stream-chat-react";

export const SupportChannel = () => {
  const { client } = useChatContext();
  const [channel, setChannel] = useState<StreamChannel>();

  useEffect(() => {
    client.channel("messaging", "support").watch().then(setChannel);
  }, [client]);

  if (!channel) return <ChannelPlaceholder>Loading...</ChannelPlaceholder>;

  return (
    <Channel channel={channel}>
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};
```

`client.channel()` returns the channel the client holds for that id, or creates it, without a request. `Channel.watch()` is safe to call again, even from several places at once: it sends a request only when the channel isn't loaded yet, or when you pass options it wasn't watched with. So an effect that runs twice, or two components opening the same channel, still send one query. It resolves with the channel to use from then on. Pass what you passed as `channelQueryOptions` to `Channel.watch(options)`.

Channels that come from `ChannelList`, or from a `client.queryChannelsAndHydrate()` call, are already watched. If yours come from there, there is nothing to change. Channels from search results aren't watched. [watch and query](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/shared-entities/#watch-and-query) covers the ways to load a channel.

Two things to plan for:

- **A channel that isn't loaded renders as an empty channel,** with no error, because nothing failed. If a channel shows nothing, check `channel.initialized` first.
- **Loading and error UI is yours.** `Channel` no longer renders `LoadingIndicator` or `LoadingErrorIndicator` for a query it doesn't make. Render them around `Channel`, in your own layout. The `LoadingErrorIndicator` override key is removed, because no component renders it; the `LoadingErrorIndicator` component is still exported.

### Channel no longer writes document.title

`Channel` used to add the channel's unread count to the tab title when a message arrived while the tab was hidden, and `activeUnreadHandler` let you change that. Both are removed, with no replacement in the SDK.

What belongs in a tab title depends on what your app shows, which no SDK component knows. It knows the unread counts, but not whether the user is looking at the channel they're in. The version that shipped counted only the open channel, did nothing in the threads view, skipped thread replies, and stacked prefixes across channel switches.

Two things to know when you write your own:

- The counts belong to the user, not to a view. The total of unread messages comes from the connection's `me` payload and the `total_unread_count` on events. The number of unread threads comes from `client.threads`. Neither needs an open channel or thread.
- They count different things. One counts unread messages in channels, the other counts threads with unread replies. Don't add them together.

### ChannelProps.channel is required; EmptyPlaceholder is gone

A `Channel` without a channel had nothing to provide, so the prop is required and `EmptyPlaceholder` is removed. Check for a channel where you render it, or render `ChannelPlaceholder`, the channel's container with no channel in it, in the same place:

```tsx
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  ChannelPlaceholder,
  MessageComposer,
  MessageList,
} from "stream-chat-react";

export const ChannelPanel = ({ channel }: { channel?: StreamChannel }) =>
  channel ? (
    <Channel channel={channel}>
      <MessageList />
      <MessageComposer />
    </Channel>
  ) : (
    <ChannelPlaceholder>Select a conversation</ChannelPlaceholder>
  );
```

### Window is gone

`Window` wrapped the channel's header, message list and composer, and made room for an open thread. It's removed. Render those parts directly inside `Channel`, and render the thread where your layout wants it: in a `ThreadSlot` with `ChatView`, or in your own panel with `Thread`. [Channel UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/channel-ui/) shows a channel column.

### Channel state is one store

In `stream-chat` 9, `channel.state` was a plain object. Its fields, such as `read` and `members`, changed without notifying anyone, and components re-rendered because `Channel` copied them into `ChannelStateContext` on every event. In `stream-chat` 10, `channel.state` is a store, and nothing copies it. Subscribe to it with `useStateStore`.

In v14:

```tsx
const { read } = channel.state;
```

In v15:

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

const readSelector = (state: ChannelStateData) => ({ read: state.read });

export const ReadCount = () => {
  const channel = useChannel();
  const { read } = useStateStore(channel.state, readSelector);
  return <span>{Object.keys(read).length} members have read state</span>;
};
```

Most keys keep their v9 names, such as `members`, `membership`, `read`, `typing` and `watchers`. `watcher_count` is `watcherCount`. The messages and the pinned messages moved to `channel.messagePaginator` and `channel.pinnedMessagesPaginator`. The state also holds values that used to live elsewhere: `data`, `memberCount`, `muteStatus`, `watchStatus`, `initialized` and `aiState`. Getters such as `channel.state.members` still work, but they return a snapshot and re-render nothing. [What channels and threads hold](https://getstream.io/chat/docs/sdk/react/v15/state/channels-and-threads/what-channels-and-threads-hold/) lists the fields.

Two more renames:

- `channel.disconnected` is `channel.pendingDisposal`, which is read-only.
- `Channel.getConfig()` is the `channel.serverConfig` getter: drop the parentheses. It's the channel type's settings from the server. What the client enforces is `channel.config`, the server's settings combined with yours, which the `channel.configState` store also publishes.

The configuration a v14 component read from `ChannelStateContext` as `channelConfig` is `channel.configState`, with new field names:

| v14 `channelConfig`      | v15 `channel.configState`      |
| ------------------------ | ------------------------------ |
| `typing_events`          | `typingEvents.enabled`         |
| `read_events`            | `readEvents.enabled`           |
| `replies`                | `replies.enabled`              |
| `user_message_reminders` | `userMessageReminders.enabled` |
| `commands`               | `availableCommands`            |

`AIStates` moved too: import it from `stream-chat`, not `stream-chat-react`.

### The channel's latest message

`channel.state.latestMessages` and `Channel.lastMessage()` are removed. The channel's newest message is in its message paginator's `aggregateState`, whichever interval of messages is on screen.

In v14:

```tsx
const last = channel.state.latestMessages.at(-1);
```

In v15:

```tsx
import type { MessagePaginatorAggregateState } from "stream-chat";
import { useChannel, useStateStore } from "stream-chat-react";

const latestMessageSelector = (state: MessagePaginatorAggregateState) => ({
  latestMessage: state.latestMessage,
});

export const LastMessageText = () => {
  const channel = useChannel();
  const { latestMessage } = useStateStore(
    channel.messagePaginator.aggregateState,
    latestMessageSelector,
  );
  return <span>{latestMessage?.text}</span>;
};
```

`ChatContext.latestMessageDatesByChannels` is removed too. It held when the current user last posted in each channel, for the slow-mode cooldown, which is now `channel.cooldownTimer`. `useCooldownRemaining()` returns the seconds left, and `useIsCooldownActive()` whether the user is still waiting.

### ChatContext has no active channel

`ChatContext` no longer carries `channel`, `setActiveChannel` or `channelsQueryState`. Several `Channel` components can be mounted at once, each bound through its `channel` prop, so there is no single selected channel. Each `Channel` activates its own channel with `Channel.activate()`, and several can be active at once; see [Entity lifecycle](https://getstream.io/chat/docs/sdk/react/v15/state/state-and-entities/entity-lifecycle/).

In v14:

```tsx
const { setActiveChannel } = useChatContext();
setActiveChannel(nextChannel);
```

In v15:

```tsx
import type { Channel as StreamChannel } from "stream-chat";
import { useWorkspaceNavigation } from "stream-chat-react";

export const OpenChannelButton = ({ channel }: { channel: StreamChannel }) => {
  const { openChannel } = useWorkspaceNavigation();
  return <button onClick={() => openChannel(channel)}>Open</button>;
};
```

`useWorkspaceNavigation()` also has `isChannelActive(cid)`, `openChannels`, and the same for threads. Without a layout such as `ChatView`, it does nothing, so a component can call it anywhere. The first call of each open, close and back operation then logs a console warning. An app without a layout keeps the selected channel in its own state and passes it to `Channel`. See [Navigation](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/navigation/).

To show a channel from elsewhere, such as a deep link or a search result, in the channel lists, call `client.channelManager.ingestChannel(channel)` before you open it.

## Channel lists

`ChannelList` no longer queries channels. Its data comes from a `ChannelPaginator` that you create and register on `client.channelManager`, and the component renders what that paginator holds. Its props are `paginator`, `loadMoreDebounceMs` and `loadMoreThresholdPx`.

In v14:

```tsx
<ChannelList
  filters={filters}
  sort={{ last_message_at: -1 }}
  options={{ presence: true, state: true }}
/>
```

In v15:

```tsx
import { useEffect, useMemo } from "react";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import { ChannelList, useChatContext } from "stream-chat-react";

const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];

export const MyChannelList = ({ userId }: { userId: string }) => {
  const { client } = useChatContext();

  const paginator = useMemo(() => {
    const filters: ChannelFilters = {
      members: { $in: [userId] },
      type: "messaging",
    };
    return new ChannelPaginator({
      client,
      filters,
      id: "channels:default",
      paginatorOptions: { pageSize: 10 },
      requestOptions: { presence: true, state: true },
      sort,
    });
  }, [client, userId]);

  useEffect(() => {
    client.channelManager.insertPaginator({ paginator });
    return () => {
      client.channelManager.removePaginator(paginator);
    };
  }, [client, paginator]);

  return <ChannelList paginator={paginator} />;
};
```

Registering matters. `client.channelManager` routes events into the lists it holds. A paginator that's only passed as a prop shows its first page, and then never adds, moves or removes a channel as events arrive.

The query options split in two. Options of the request, such as `presence`, `state` and `watch`, go in `requestOptions`. Paging, such as `pageSize`, goes in `paginatorOptions`. `offset` and `limit` aren't accepted: the paginator owns them.

`ChannelNavigation` renders one `ChannelList` for each paginator on `client.channelManager`, with the list header and the search bar. [Channel lists](https://getstream.io/chat/docs/sdk/react/v15/features/channels/channel-lists/) shows a sidebar built that way.

### Props with no direct replacement

| v14 prop                                                                         | v15                                                                                                                                                                                                                         |
| -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `filters`, `sort`, `options`                                                     | the `filters`, `sort` and `requestOptions` of `ChannelPaginator`                                                                                                                                                            |
| `channelRenderFilterFn`, `renderChannels`, `lockChannelOrder`                    | the paginator's `filters` and `sort`. The order comes from the sort, so a list can't be re-ordered locally                                                                                                                  |
| `onMessageNew`, `onChannelUpdated`, `onAddedToChannel` and the other `on*` props | `client.channelManager.addEventHandler(...)`. See [Customizing event handling](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/channel-list-paginators/#customizing-event-handling)                                             |
| `allowNewMessagesFromUnfilteredChannels`                                         | the `ignoreEventsForUnknownChannels` handler from `stream-chat`, inserted first. See [Channels the client hasn't seen](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/channel-list-paginators/#channels-the-client-hasnt-seen) |
| `customQueryChannels`                                                            | removed. The paginator sends the query                                                                                                                                                                                      |
| `Paginator`, `EmptyStateIndicator`                                               | the `EmptyListIndicator`, `FirstPageLoadingIndicator`, `LoadingNextPageIndicator` and `EndReachedIndicator` overrides on `WithComponents`                                                                                   |
| `getLatestMessagePreview`                                                        | the `SummarizedMessagePreview` override                                                                                                                                                                                     |
| `showChannelSearch`                                                              | render `ChannelNavigation`, which includes the search bar, or `Search` where your layout needs it                                                                                                                           |
| `setActiveChannelOnMount`, `customActiveChannel`                                 | open a channel with `useWorkspaceNavigation().openChannel(channel)`                                                                                                                                                         |
| `sendChannelsToList`                                                             | read the paginator's channels with `useChannelPaginatorState(paginator)`                                                                                                                                                    |
| `recoveryThrottleIntervalMs`                                                     | removed. The client reloads the registered lists after a reconnect                                                                                                                                                          |
| `watchers`                                                                       | removed                                                                                                                                                                                                                     |

The channel-list helpers `moveChannelUpwards`, `isChannelPinned`, `isChannelArchived`, `findLastPinnedChannelIndex`, `extractSortValue`, `shouldConsiderPinnedChannels` and `shouldConsiderArchivedChannels` are removed, along with `usePaginatedChannels`. A list no longer moves a channel to the top when a message arrives: a channel's place comes from the sort, so sort by `last_message_at` to keep the latest activity first.

### More than one list

Register several paginators, and let the manager decide which list holds a channel that matches more than one:

```ts
client.channelManager.setPaginators([pinned, archived, muted, everythingElse]);
client.channelManager.setOwnershipResolver([
  "channels:archived",
  "channels:muted",
]);
```

`client.channelManager.setPaginators()` publishes one update for the whole set. Use `client.channelManager.insertPaginator()` and `client.channelManager.removePaginator()` for changes one at a time. [Channels that match several paginators](https://getstream.io/chat/docs/sdk/react/v15/state/loading-data/channel-list-paginators/#channels-that-match-several-paginators) explains the ownership rules.

### ChannelListContext holds a paginator

`channels`, `setChannels`, `hasNextPage` and `loadNextPage` are gone. The context gives a `ChannelPaginator`, and `useChannelPaginatorState` reads it. `ChannelLists`, which `ChannelNavigation` renders, provides the first paginator registered on `client.channelManager`. A `ChannelList` you render yourself doesn't provide it: wrap it in `ChannelListContextProvider`.

In v14:

```tsx
const { channels, setChannels } = useChannelListContext();
```

In v15:

```tsx
import type { ChannelPaginator } from "stream-chat";
import {
  useChannelListContext,
  useChannelPaginatorState,
} from "stream-chat-react";

const LoadedChannelCount = ({ paginator }: { paginator: ChannelPaginator }) => {
  const { channels } = useChannelPaginatorState(paginator);
  return <span>{channels?.length ?? 0} channels</span>;
};

export const ChannelCount = () => {
  const { paginator } = useChannelListContext();
  return paginator ? <LoadedChannelCount paginator={paginator} /> : null;
};
```

To change the loaded list, call `ChannelPaginator.setItems({ valueOrFactory })`. To load the next page, call `ChannelPaginator.toTail()`.

### Channel-list UI overrides

`ChannelListUI` is removed. The list body and its states are separate overrides on `WithComponents`: `ListItem`, `EmptyListIndicator`, `FirstPageLoadingIndicator`, `LoadingNextPageIndicator` and `EndReachedIndicator`. `SummarizedMessagePreview` renders a row's message preview. See [Channel list UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/channel-list-ui/).

`ChannelListItem` lost `channelUpdateCount`, `getLatestMessagePreview`, `setActiveChannel`, `watchers` and its own `key` prop. React's `key` is unaffected. Pass `previewedMessage` to preview a message other than the latest, such as a search match. `ChannelListItemUI` takes `previewedMessage` instead of `lastMessage` and `latestMessagePreview`. When the channel type skips system messages for the last message, the preview skips them too, so it agrees with the list's order.

## Threads

### Thread takes a thread and renders its children

`Thread` no longer renders a header, a message list and a composer of its own. Like `Channel`, it takes the thread as a required `thread` prop, gives it to its children, loads it when needed, and renders the children you pass. You no longer wrap it in `ThreadProvider`.

In v14:

```tsx
<Thread
  additionalMessageComposerProps={{ audioRecordingEnabled: true }}
  additionalMessageListProps={{ onUserClick }}
  virtualized
/>
```

In v15:

```tsx
import type { Thread as StreamThread } from "stream-chat";
import {
  MessageComposer,
  Thread,
  ThreadHeader,
  VirtualizedMessageList,
} from "stream-chat-react";

export const ThreadPanel = ({ thread }: { thread: StreamThread }) => (
  <Thread thread={thread}>
    <ThreadHeader />
    <VirtualizedMessageList />
    <MessageComposer audioRecordingEnabled focus />
  </Thread>
);
```

Get the thread from `client.threads.ensure({ channel, parentMessage })` or `client.threads.get(id)`. With `ChatView`, `ThreadSlot` renders `Thread` for the thread in its slot, and its children are the thread's contents. [Thread UI](https://getstream.io/chat/docs/sdk/react/v15/ui/components/thread-ui/) covers both.

Where the removed props went:

| v14 prop on `Thread`                    | v15                                                                 |
| --------------------------------------- | ------------------------------------------------------------------- |
| `additionalMessageComposerProps`        | props on your `MessageComposer`                                     |
| `additionalMessageListProps`            | props on your `MessageList`                                         |
| `additionalVirtualizedMessageListProps` | props on your `VirtualizedMessageList`                              |
| `additionalParentMessageProps`          | a `ThreadHead` override on `WithComponents`                         |
| `autoFocus`                             | `<MessageComposer focus />`                                         |
| `virtualized`                           | render the list you want                                            |
| `enableDateSeparator`                   | removed. Pass `withDateSeparator` to the list                       |
| `Message`                               | a `MessageUI` override, set with `WithComponents` around the thread |
| `messageActions`                        | removed. See [Message actions](#message-actions)                    |

What follows from it:

- **Date separators.** v14's `Thread` turned them off for the list it rendered. A `MessageList` in a thread now shows them, unless you pass `withDateSeparator={false}`.
- **The parent message** is rendered by the message list, above the replies, because it scrolls with them. The `head` prop of `MessageList` and `VirtualizedMessageList` is removed. To change how the parent renders, set a `ThreadHead` override.
- **The header.** `ComponentContext.ThreadHeader` is removed: render the header you want. `ThreadHeader` takes `overrideTitle`, `StartContent` and `EndContent`. Its `thread` and `closeThread` props are removed. It closes the thread through workspace navigation, and `useCloseThread()` gives a custom header the same behaviour.
- **No remount per thread.** `Thread` no longer rebuilds its children when the thread changes. A component inside it that reset its state on mount must depend on the thread instead.
- `useLegacyThreadContext` is removed. The thread's composer reads the thread from `ThreadContext`.

## Layout

### ChatView is a slot layout

`ChatView` moved to `stream-chat-react/slot-layout`, and its compound components are gone. `ChatView.Channels` and `ChatView.Threads` are replaced by the `views` prop, and `ChatView.ThreadAdapter` by `ThreadSlot`. `layouts` declares the slots of each view. `useThreadsViewContext` is removed.

In v14:

```tsx
<ChatView>
  <ChatView.Selector />
  <ChatView.Channels>
    <ChannelList filters={filters} />
    <Channel>
      <Window>
        <MessageList />
        <MessageComposer />
      </Window>
      <Thread />
    </Channel>
  </ChatView.Channels>
  <ChatView.Threads>
    <ThreadList />
    <ChatView.ThreadAdapter>
      <Thread />
    </ChatView.ThreadAdapter>
  </ChatView.Threads>
</ChatView>
```

In v15:

```tsx
import {
  ChannelHeader,
  ChannelNavigation,
  MessageComposer,
  MessageList,
  ThreadHeader,
  ThreadList,
} from "stream-chat-react";
import {
  ChannelSlot,
  ChatView,
  ThreadSlot,
} from "stream-chat-react/slot-layout";
import type { LayoutDescriptor } from "stream-chat-react/slot-layout";

// Declared outside the component, so ChatView gets the same array on every render.
const layouts: LayoutDescriptor[] = [
  { id: "channels", slots: ["channel", "thread"] },
  { id: "threads", slots: ["thread"] },
];

const ThreadPanel = () => (
  <ThreadSlot slot="thread">
    <ThreadHeader />
    <MessageList />
    <MessageComposer />
  </ThreadSlot>
);

const ChannelsView = () => (
  <>
    <ChatView.Selector />
    <ChannelNavigation />
    <ChannelSlot slot="channel">
      <ChannelHeader />
      <MessageList />
      <MessageComposer />
    </ChannelSlot>
    <ThreadPanel />
  </>
);

const ThreadsView = () => (
  <>
    <ChatView.Selector />
    <ThreadList />
    <ThreadPanel />
  </>
);

export const Workspace = () => (
  <ChatView
    layouts={layouts}
    views={{ channels: <ChannelsView />, threads: <ThreadsView /> }}
  />
);
```

Pass `layouts` with `views`. Without it, each view has a single slot that holds its list, and channels and threads have nowhere to open. [Workspace with ChatView](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/) explains views, slots and where an opened channel lands.

`ChatView.Selector`, `useActiveThread` and `useChatViewContext` are imported from `stream-chat-react/slot-layout` too. `useChatViewContext` throws outside `ChatView`.

### New package entry points

The layout parts have their own entry points, so an app that doesn't use them doesn't load them:

| Import from                       | What it holds                                                                                                                                                              |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `stream-chat-react`               | `useWorkspaceNavigation`, `ChannelNavigation` and everything else in the core                                                                                              |
| `stream-chat-react/slot-layout`   | `ChatView` and its selector, `WorkspaceLayout`, `Slot`, `ChannelSlot`, `ThreadSlot`, `ThreadListSlot`, `useActiveThread`, `useChatViewContext` and `useChatViewNavigation` |
| `stream-chat-react/slot-geometry` | `SlotGeometryProvider`, `useSlotGeometry` and `useRegisterSlotGeometry`                                                                                                    |

The stylesheets didn't move: `stream-chat-react/dist/css/index.css` and the other files in `dist/css/` are the same as in v14, and `index.css` styles the slot layout too. [Entry points and CSS files](https://getstream.io/chat/docs/sdk/react/v15/reference/entry-points-and-css-files/) lists every entry point.

## Components

### Removed contexts

`ChannelStateContext` and `ChannelActionContext` are removed, along with `useChannelStateContext`, `useChannelActionContext` and their providers. So are `TypingContext`, `useTypingContext` and `TypingProvider`. Channel state is no longer copied into React context. Read it from the channel.

In v14:

```tsx
const { channel, messages, members } = useChannelStateContext();
```

In v15:

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

const itemsSelector = (state: MessagePaginatorState) => ({
  messages: state.items,
});
const membersSelector = (state: ChannelStateData) => ({
  members: state.members,
});

export const ChannelSummary = () => {
  const channel = useChannel();
  const { messages } = useStateStore(
    channel.messagePaginator.state,
    itemsSelector,
  );
  const { members } = useStateStore(channel.state, membersSelector);

  return (
    <span>
      {messages?.length ?? 0} messages loaded, {Object.keys(members).length}{" "}
      members
    </span>
  );
};
```

`useChannel()` takes no arguments. It returns the channel of the surrounding `Channel`, or the thread's channel inside a `Thread`, and throws outside both. Inside a thread, `channel.messagePaginator` is the channel's list, not the thread's: use `useMessagePaginator()`, which returns the thread's list there. [Which owner a hook reads](https://getstream.io/chat/docs/sdk/react/v15/ui/reading-state-in-components/#which-owner-a-hook-reads) shows how to pick the right one.

Where the common v14 values are:

| v14                                                               | v15                                                                                                                                                                                     |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channel`                                                         | `useChannel()`                                                                                                                                                                          |
| `messages`                                                        | `items` in `channel.messagePaginator.state`                                                                                                                                             |
| `hasMore`, `hasMoreNewer`, `loadingMore`                          | `hasMoreTail` (older), `hasMoreHead` (newer) and `isLoading` in the same state                                                                                                          |
| `members`, `read`, `watchers`, `watcher_count`                    | `members`, `read`, `watchers` and `watcherCount` in `channel.state`                                                                                                                     |
| `typing`, from `useTypingContext()`                               | `typing` in `channel.state`. See [Typing indicators](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/typing-indicators/)                                                                    |
| `channelCapabilities`                                             | `useChannelCapabilities()`                                                                                                                                                              |
| `channelConfig`                                                   | `channel.configState`. See [Channel state is one store](#channel-state-is-one-store)                                                                                                    |
| `pinnedMessages`                                                  | `channel.pinnedMessagesPaginator`                                                                                                                                                       |
| `thread`, `threadMessages`, `openThread`, `closeThread`           | `useThreadContext()`, `thread.messagePaginator`, and `openThread` and `closeThread` from `useWorkspaceNavigation()`                                                                     |
| `loadMore`, `loadMoreNewer`                                       | `MessagePaginator.toTail()` and `MessagePaginator.toHead()`                                                                                                                             |
| `jumpToMessage(id)`, `jumpToLatestMessage()`                      | `MessagePaginator.jumpToMessage(id)` and `MessagePaginator.jumpToTheLatestMessage()`                                                                                                    |
| `sendMessage`, `editMessage`, `retrySendMessage`, `deleteMessage` | `MessageComposer.send()` and `MessageComposer.update()`, and `channel.messageOperations`. See [Message operations](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/message-operations/) |
| `markRead`                                                        | `Channel.markReadViaReporter()`                                                                                                                                                         |
| `mutes`, from `MessageContext`                                    | `mutes` from `useChatContext()`                                                                                                                                                         |

### Context hooks take no component name, and throw outside their provider

The optional `componentName` argument is removed from `useChatContext`, `useTranslationContext`, `useMessageContext`, `useComponentContext`, `useMessageComposerContext`, `useMessageListContext` and `useMessageBounceContext`. Write `useChatContext()`, not `useChatContext("MyComponent")`. TypeScript flags every call. Plain JavaScript ignores the argument.

In v14 most context hooks returned an empty object outside their provider, some with a warning in the console. In v15 a hook whose provider is required throws, and the error names the provider it needs. This affects, among others, `useChatContext` outside `Chat`, `useMessageContext` outside a message, `useMessageComposerContext` outside a composer, and `useChatViewContext` outside `ChatView`. A component that renders both inside and outside a channel reads `useChannelInstanceContext()`, which returns `{}` outside a `Channel`. `useTranslationContext` and `useComponentContext` still work anywhere.

### Message UI overrides use MessageUI

The deprecated `Message` override key is removed, and so is every `Message` prop that took a message UI component. Set the `MessageUI` override:

| v14                                                     | v15                                                            |
| ------------------------------------------------------- | -------------------------------------------------------------- |
| `Message` key on `WithComponents`                       | `MessageUI`                                                    |
| `<MessageList Message={CustomMessage} />`               | the `MessageUI` override                                       |
| `<VirtualizedMessageList Message={CustomMessage} />`    | the `VirtualMessage` override, which falls back to `MessageUI` |
| `<Message Message={...} />`, `<Thread Message={...} />` | the `MessageUI` override, set around the part it applies to    |

```tsx
import type { PropsWithChildren } from "react";
import {
  MessageText,
  WithComponents,
  useMessageContext,
} from "stream-chat-react";

const CustomMessage = () => {
  const { message } = useMessageContext();
  return (
    <div className="custom-message">
      <strong>{message.user?.name}</strong>
      <MessageText />
    </div>
  );
};

// Declared outside the component, so WithComponents gets the same object on every render.
const overrides = { MessageUI: CustomMessage };

export const WithCustomMessages = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);
```

A custom message UI receives no props: it reads everything from `useMessageContext()`. In TypeScript, a leftover `Message` key or prop is a compile error, and for the key the error suggests `MessageUI`. In plain JavaScript it's ignored, and the default message UI renders, so search your code for `Message=` and `Message:`. See [Overriding components](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/overriding-components/).

### Date separators: withDateSeparator

`MessageList` and `VirtualizedMessageList` took `disableDateSeparator`, while `Thread` took `enableDateSeparator`. Both lists take `withDateSeparator` now, and `Thread`'s prop is removed. The defaults behave as before:

| Component                | v14                            | v15                         |
| ------------------------ | ------------------------------ | --------------------------- |
| `MessageList`            | `disableDateSeparator={false}` | `withDateSeparator={true}`  |
| `VirtualizedMessageList` | `disableDateSeparator={true}`  | `withDateSeparator={false}` |

The helpers took the rename too: `processMessages` takes `withDateSeparator` instead of `enableDateSeparator`, and `useEnrichedMessages` instead of `disableDateSeparator`.

### Message list props

Besides `Message`, `head` and the date separator prop, the lists dropped the props that `Channel` used to fill in. `MessageList` dropped `loadMore`, `loadMoreNewer`, `loadingMoreForJumpToChannelMessage`, `openThread`, `retrySendMessage` and `threadList`. `VirtualizedMessageList` dropped `loadMore`, `loadMoreNewer`, `openThread` and `threadList`, and also `hasMore`, `hasMoreNewer`, `loadingMore`, `loadingMoreNewer`, `highlightedMessageId`, `channelUnreadUiState` and `messageLimit`. Each list reads its message paginator, and knows from its context whether it shows a thread. `MessageList` also dropped `hasMore`, `jumpToLatestMessage`, `loadingMoreNewer`, `messageLimit` and `messages`, and `VirtualizedMessageList` dropped `messages`: each list renders the messages of its paginator. `MessageList` keeps `loadingMore`. Without it, the list shows the `LoadingIndicator` override while it loads an older page; `loadingMore={false}` hides it.

### Message actions

The `messageActions` prop is removed from `MessageList`, `VirtualizedMessageList`, `Thread` and `Message`, together with `MESSAGE_ACTIONS`, `OPTIONAL_MESSAGE_ACTIONS`, `getMessageActions()`, and the `MessageActionsArray` and `Capabilities` types. In v14 the prop had already stopped changing which actions showed. `MessageContext` no longer has `getMessageActions` or `actionsEnabled`.

To choose the actions, filter `defaultMessageActionSet` and pass the result to `MessageActions`:

```tsx
import type { PropsWithChildren } from "react";
import {
  MessageActions,
  WithComponents,
  defaultMessageActionSet,
} from "stream-chat-react";

// The dropdown toggle has no type, so keep it by its placement.
const actionSet = defaultMessageActionSet.filter(
  (item) =>
    item.placement === "quick-dropdown-toggle" || item.type !== "delete",
);

const MessageActionsWithoutDelete = () => (
  <MessageActions messageActionSet={actionSet} />
);

const overrides = { MessageActions: MessageActionsWithoutDelete };

export const WithoutDelete = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);
```

The action types the SDK ships are the `DefaultMessageActionType` union. Actions the user can't perform are still left out for you. See [Actions](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/actions/).

### Composer

- `overrideSubmitHandler` is removed from `MessageComposer`. To change the request, register a `sendMessageRequest` handler, as in [Channel props that moved](#channel-props-that-moved). To change the message before it's sent, add a [composition middleware](https://getstream.io/chat/docs/sdk/react/v15/state/writing-messages/composer-middleware/).
- `preventClearingOnUnmount` is removed. `MessageComposer` no longer clears a composer you supplied through `MessageComposerControllerProvider` when it unmounts. Call `MessageComposer.clear()` on it yourself when you're done with it.
- `MessageComposerContext` no longer has `handleSubmit`. A custom submit control calls the function `useMessageComposerSubmitFn()` returns, which sends a new message or saves an edit. To send from your own code, call `MessageComposer.send()`, or `MessageComposer.update()` for an edit. Each resolves to `"sent"`, `"nothing-to-send"` or `"failed"`.
- `useAttachmentManagerState()` returns `customCdn` instead of `hasCustomDoUploadRequest`. If your upload function sends files to storage Stream doesn't host, declare it with `client.config.set({ messageComposer: { attachments: { customCdn: true } } })`, or the file action disappears for users without the `upload-file` capability.
- `UnsupportedAttachmentPreview` and `RemoveAttachmentPreviewButton` are exported, for custom attachment previews to reuse.

### Headers

`ComponentContext.HeaderStartContent` is removed. `ChannelHeader` and `ThreadHeader` take `StartContent` and `EndContent` props instead. By default, `StartContent` is a back button when the panel is stacked over other content, and `EndContent` is a close button when the panel can be closed:

```tsx
import {
  ChannelHeader,
  WorkspaceNavigationBackButton,
} from "stream-chat-react";

// SidebarToggle stands for a control of your own.
const SidebarToggle = () => <button type="button">Menu</button>;

const HeaderStart = () => (
  <>
    <WorkspaceNavigationBackButton />
    <SidebarToggle />
  </>
);

export const Header = () => <ChannelHeader StartContent={HeaderStart} />;
```

A prop replaces its default button, so render the button too if you want to keep it. `HeaderEndContent` stays on `ComponentContext`, for the end of the channel-list and thread-list headers.

### Audio playback

`allowConcurrentAudioPlayback` is removed from `Channel`, and `allowConcurrentPlayback` from `WithAudioPlayback`. One voice message plays at a time across the app: starting one pauses the other. Switching channels stops the previous channel's audio. `useActiveAudioPlayer()` reports the player that's active anywhere in the app.

### Search

Channel search results, and the channels behind message search results, are no longer watched. The built-in result items watch the channel when the user opens it. A custom result item calls `Channel.watch()`, or `MessagePaginator.jumpToMessage(id, { watchChannel: true })` for a message.

`Channel` no longer scrolls to a message picked in search. The result item calls `MessagePaginator.jumpToMessage()` itself, and the highlight is in `channel.messagePaginator.messageFocusSignal`. See [Search](https://getstream.io/chat/docs/sdk/react/v15/features/channels/search/).

## Styling

The stylesheet paths and the `--str-chat__*` variables are the same as in v14. A few things changed around them:

- **Wrappers.** `.str-chat__main-panel`, which `Window` rendered, and the channel's inner `.str-chat__container` are gone, and `customClasses.chatContainer` is removed with them. The header, the message list and the composer sit directly in `.str-chat__channel`. Move CSS that targets either wrapper to `.str-chat__channel`.
- **The channel list inside `ChatView`.** Inside `ChatView`, the channel list renders no `str-chat` element of its own and takes the tokens from `ChatView`'s root, so a token you override there reaches the list. CSS that matched the list's root as `.str-chat` inside `ChatView`, such as `.str-chat.str-chat__channel-list`, no longer matches it. Outside `ChatView`, `ChannelNavigation`'s root, or a wrapper around a `ChannelList` rendered on its own, carries `str-chat` and the `theme` classes. `customClasses.channelList` replaces `str-chat__channel-list` on `ChannelNavigation`'s root. See [Design tokens](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/theming/#design-tokens).
- **Quoted messages.** The quoted message preview, in the composer and inside messages, uses the text colour that goes with its background, `--str-chat__chat-text-incoming`, or `--str-chat__chat-text-outgoing` for your own messages. If you overrode those colours to fix the contrast, remove the override.
- **Buttons in dark mode.** The dark theme sets its own button text colours: dark text on solid primary and destructive buttons, and lighter text on outline and ghost buttons.

[Theming](https://getstream.io/chat/docs/sdk/react/v15/ui/customizing/theming/) lists the variables.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/release-guides/upgrade-to-v15/](https://getstream.io/chat/docs/sdk/react/v15/release-guides/upgrade-to-v15/).