# Slot layout

> **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/).

The slot layout is the model behind `ChatView`: its views, the slots in each view, and what each slot holds. This page lists its types, the layout controller, and the hooks that read and change it. [Workspace with ChatView](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/) explains how it behaves.

Everything on this page imports from `stream-chat-react/slot-layout`. The navigation interface the SDK's components use, `useWorkspaceNavigation()`, imports from `stream-chat-react`: see [Workspace navigation API](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/workspace-navigation-api/).

## The pieces

| Piece                                                                    | What it does                                                                                  | Reference                                                                                                 |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `ChatView`                                                               | Creates the layout controller, provides the navigation, renders the shown view.               | [ChatView](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/chatview-components/#chatview)             |
| The layout controller                                                    | Holds the layout's state and the methods that change it. Read it from `useChatViewContext()`. | [The layout controller](#the-layout-controller)                                                           |
| `useChatViewNavigation()`                                                | Opens, stacks, hides and closes things in slots, following the navigation rules.              | [useChatViewNavigation](#usechatviewnavigation)                                                           |
| `Slot`, `WorkspaceLayout`, `ChannelSlot`, `ThreadSlot`, `ThreadListSlot` | Render what slots hold.                                                                       | [ChatView components](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/chatview-components/)           |
| `WorkspaceNavigationAdapter`                                             | Implements the `WorkspaceNavigation` interface over the slots. `ChatView` renders it.         | [Workspace navigation API](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/workspace-navigation-api/) |

The layout controller knows which slots exist and what they hold. It doesn't know where they are on screen or how wide they are: the app's CSS places them, and [Slot geometry](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/slot-geometry/) reports what the CSS did.

## Binding, not mounting

A slot holds a **binding**: the kind of thing it shows, a key, and the thing itself. Opening the channel `messaging:design` binds `{ kind: "channel", key: "messaging:design", source: channel }` to a slot, and the component that renders the slot reads it from there. [Where an opened channel or thread lands](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#where-an-opened-channel-or-thread-lands) says which slot.

A binding you pass is a `ChatViewEntityBinding`: `{ key?: string; kind; source }`. These are the kinds:

| `kind`               | `source`             | Rendered by default by `Slot`                               |
| -------------------- | -------------------- | ----------------------------------------------------------- |
| `channel`            | `Channel`            | nothing: use `ChannelSlot`, or pass `slotRenderers.channel` |
| `thread`             | `Thread`             | nothing: use `ThreadSlot`, or pass `slotRenderers.thread`   |
| `channelList`        | `{}`                 | `ChannelNavigation`                                         |
| `threadList`         | `{}`                 | `ThreadList`                                                |
| `userProfile`        | `{ userId: string }` | the user's id                                               |
| `memberList`         | `Channel`            | nothing                                                     |
| `pinnedMessagesList` | `Channel`            | nothing                                                     |
| `searchResults`      | `{ query: string }`  | nothing                                                     |
| `userList`           | `{ query: string }`  | nothing                                                     |

- **The key** is what the layout recognizes the channel or thread by: the channel's `cid`, or the thread's `id`. The duplicate check and the navigation's `isChannelActive`, `closeChannel` and the others compare it. A binding without a key is never a duplicate.
- **A `channel` binding** opens in the channels view, switching to it first. Every other kind opens in the view that's shown.
- **A `channelList` or `threadList` binding** makes its slot a [list slot](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#list-slots): navigation never picks it on its own, and `close` and `release` never empty it.

The layout controller stores bindings in another shape, `LayoutSlotBinding`: `{ key?: string; payload: unknown }`, with the `ChatViewEntityBinding` as its payload. `useLayoutViewState()` returns that shape. Three functions convert:

| Function                                                  | Returns                                                                                                   |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `createChatViewSlotBinding(entity)`                       | The `LayoutSlotBinding` for a `ChatViewEntityBinding`. Its key is `"<kind>:<key>"`.                       |
| `getChatViewEntityBinding(binding?)`                      | The `ChatViewEntityBinding` in a stored binding, or `undefined`.                                          |
| `createThreadEntityBinding(client, { channel, message })` | A `thread` binding for the thread of a parent message. It gets the thread from `client.threads.ensure()`. |

### useChatViewNavigation

`useChatViewNavigation()` changes what slots hold, following the same rules as the SDK's components. Every member acts on the shown view, and does nothing for a slot the view doesn't have. Outside a `ChatView`, `open` returns a rejection and the other members do nothing.

| Member      | Type                                                                            | Description                                                                                                                                                                                                                                                                                                                  |
| ----------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open`      | `(binding: ChatViewEntityBinding, options?: ChatViewOpenOptions) => OpenResult` | Opens the binding in a slot, as described in [Where an opened channel or thread lands](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#where-an-opened-channel-or-thread-lands). An open rejected for want of a slot changes nothing. A channel rejected as a duplicate still closes the threads open beside it. |
| `openView`  | `(view: ChatView, options?: { slot?: string }) => void`                         | Shows a view. `slot` is ignored.                                                                                                                                                                                                                                                                                             |
| `pushLayer` | `(slot: string, binding: ChatViewEntityBinding) => void`                        | Stacks the binding on top of what the slot shows.                                                                                                                                                                                                                                                                            |
| `popLayer`  | `(slot: string) => void`                                                        | Removes the top layer.                                                                                                                                                                                                                                                                                                       |
| `close`     | `(slot: string) => void`                                                        | Removes the top layer. When the slot has no layers, empties it, unless it's a list slot.                                                                                                                                                                                                                                     |
| `release`   | `(slot: string) => void`                                                        | Empties the slot: what it holds, every layer on it, and its history. Does nothing for a list slot.                                                                                                                                                                                                                           |
| `hide`      | `(slot: string) => void`                                                        | Stops showing the slot. What it holds stays mounted.                                                                                                                                                                                                                                                                         |
| `unhide`    | `(slot: string) => void`                                                        | Shows a hidden slot again.                                                                                                                                                                                                                                                                                                   |

`ChatViewOpenOptions`:

| Option     | Type      | Default | Description                                                                                                            |
| ---------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
| `slot`     | `string`  |         | Opens in this slot, when the view has it. A `viewActionSlotResolvers` resolver is asked first, except with `additive`. |
| `additive` | `boolean` | `false` | Opens beside: skips the slot that already holds the same kind, and the resolvers.                                      |
| `layer`    | `boolean` | `false` | Stacks the binding on top of the target slot's content instead of replacing it.                                        |

`OpenResult` says what happened:

| `status`     | Other fields                                             | When                                                                                                                                     |
| ------------ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `"opened"`   | `slot`                                                   | It went into an empty slot. Also returned, with nothing changed, when an open with `additive` finds it already shown in the slot beside. |
| `"replaced"` | `slot`, `replaced`                                       | It replaced what the slot held. `replaced` is the old `LayoutSlotBinding`.                                                               |
| `"layered"`  | `slot`                                                   | It was stacked on top of the slot's content.                                                                                             |
| `"rejected"` | `reason`: `"no-available-slot"` or `"duplicate-binding"` | No slot can take it, because the view has none or only list slots, or the duplicate policy refused it.                                   |

[Where an opened channel or thread lands](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#where-an-opened-channel-or-thread-lands) has an example of `open` with `slot`, and [Layers](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#layers) one of `pushLayer`.

## Layers and history

A slot can hold more than one thing over time: layers stacked on top of what it holds, and a back and forward history of what it held before. [Layers and history](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#layers-and-history) explains both, and [Hiding vs closing](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#hiding-vs-closing) compares `hide`, `close` and `release`.

- **Layers** come from `pushLayer`, from `layer: true`, and from an open into a slot in use other than the main panel. While a slot has layers, `Slot` keeps what it holds and every layer mounted, and shows only the top one. Pushing the first layer and removing the last one mount what the slot holds again, so it loses its scroll position. Layers are never saved.
- **History** grows when an `open` replaces what a slot holds: the old binding goes onto the slot's back list. Opening something new clears the slot's forward list, and emptying the slot clears both. The SDK's back button removes a layer; it doesn't use the history.

### The layout controller

`useChatViewContext().layoutController` holds the layout's state. Its methods change the shown view only, and skip the checks `useChatViewNavigation()` makes: they also act on list slots. Use the controller from an effect that would otherwise depend on the navigation's functions, which change with every layout change.

| Member              | Type                                                                            | Description                                                                                                                                                           |
| ------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state`             | `StateStore<ChatViewLayoutState>`                                               | The state: `activeView`, and under `layouts`, each view's [`ChatViewLayoutViewState`](#chatviewlayoutviewstate).                                                      |
| `setActiveView`     | `(view: ChatView) => void`                                                      | Shows a view.                                                                                                                                                         |
| `openView`          | `(view: ChatView, options?: { slot?: string }) => void`                         | The same. `slot` is ignored.                                                                                                                                          |
| `bind`              | `(slot: string, binding: LayoutSlotBinding) => void`                            | Puts a binding in a slot, replacing what it holds. Clears the slot's forward history, and doesn't add to its back history.                                            |
| `openInLayout`      | `(binding: LayoutSlotBinding, options?: { targetSlot?: string }) => OpenResult` | Puts a binding in `targetSlot`, applying the duplicate policy, and adds the replaced binding to the slot's back history. Rejects without a `targetSlot` the view has. |
| `release`           | `(slot: string) => void`                                                        | Empties the slot: what it holds, its layers and its history.                                                                                                          |
| `pushLayer`         | `(slot: string, binding: LayoutSlotBinding) => void`                            | Stacks a binding on top of the slot.                                                                                                                                  |
| `popLayer`          | `(slot: string) => void`                                                        | Removes the top layer.                                                                                                                                                |
| `goBack`            | `(slot: string) => void`                                                        | Shows the slot's previous binding. The current one goes onto its forward list.                                                                                        |
| `goForward`         | `(slot: string) => void`                                                        | Shows the next binding from the forward list. The current one goes onto the back list.                                                                                |
| `hide`              | `(slot: string) => void`                                                        | Hides the slot. What it holds stays mounted.                                                                                                                          |
| `unhide`            | `(slot: string) => void`                                                        | Shows a hidden slot again.                                                                                                                                            |
| `setAvailableSlots` | `(slots: string[]) => void`                                                     | Replaces the slots of the shown view that navigation can open into.                                                                                                   |
| `setSlotNames`      | `(slots?: string[]) => void`                                                    | Replaces the shown view's declared slots, the ones its `LayoutDescriptor` lists.                                                                                      |

The methods that take a binding take a `LayoutSlotBinding`: make one with `createChatViewSlotBinding`. [Back and forward per slot](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#back-and-forward-per-slot) and [List slots](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#list-slots) have examples. The `LayoutController` class isn't exported, only its type.

## Configuring ChatView

`ChatView`'s props are listed on [ChatView components](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/chatview-components/#chatview). These are the types its layout props take.

Keep these props stable, declared outside the component or memoized. A new `layouts`, `duplicateEntityPolicy` or `resolveDuplicateEntity` starts the layout again, and everything open closes. A new `slotRenderers` or `viewActionSlotResolvers` makes every component that reads the layout or the navigation render again.

### LayoutDescriptor

`layouts` takes one `LayoutDescriptor` per view:

| Field             | Type                                             | Description                                                                                               |
| ----------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `id`              | `"channels" \| "threads"`                        | The view.                                                                                                 |
| `slots`           | `string[]`                                       | The view's slots, in order. The first slot that doesn't hold a list is the main panel.                    |
| `initialBindings` | `Partial<Record<string, ChatViewEntityBinding>>` | What slots hold at the start, by slot name. Use it to put a `channelList` or `threadList` in a list slot. |

[The built-in workspace layout](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#the-built-in-workspace-layout) has an example with `initialBindings`.

### slotRenderers

`slotRenderers` is a `ChatViewSlotRenderers`: an object with a renderer per kind. A renderer receives `ChatViewSlotRendererProps`:

| Prop     | Type                    | Description                         |
| -------- | ----------------------- | ----------------------------------- |
| `entity` | `ChatViewEntityBinding` | The binding.                        |
| `source` | the kind's `source`     | The channel, thread or other thing. |
| `slot`   | `string`                | The slot it renders in.             |

A renderer you pass replaces the default for its kind only. It's called as a plain function, not rendered as a component, so it must not call hooks: return an element, and call the hooks in that element's component. A renderer that returns `null`, `undefined` or a boolean leaves the slot empty. [`Slot` and `slotRenderers`](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#slot-and-slotrenderers) has an example.

`SlotFallback` and `slotFallbackComponents` take components that receive `ChatViewSlotFallbackProps`, `{ slot: string }`.

### Duplicates

`duplicateEntityPolicy` and `resolveDuplicateEntity` decide what happens when a channel or thread is opened into a slot while a slot of the view already holds it:

| `DuplicateEntityPolicy` | What happens                                         |
| ----------------------- | ---------------------------------------------------- |
| `"allow"`               | Both slots show it. The default.                     |
| `"move"`                | The other slot is emptied.                           |
| `"reject"`              | Nothing opens. `open` returns `"duplicate-binding"`. |

`resolveDuplicateEntity` is a `ResolveDuplicateEntity`: it decides per open, and wins over `duplicateEntityPolicy`. It receives `ResolveDuplicateEntityArgs`:

| Field             | Type                      | Description                      |
| ----------------- | ------------------------- | -------------------------------- |
| `binding`         | `LayoutSlotBinding`       | The binding being opened.        |
| `existingSlot`    | `string`                  | The slot that already holds it.  |
| `requestedSlot`   | `string \| undefined`     | The slot it's being opened into. |
| `activeViewState` | `ChatViewLayoutViewState` | The shown view's slots.          |
| `state`           | `ChatViewLayoutState`     | The whole layout state.          |

This one moves a channel that's opened again, and lets a thread show in two slots:

```tsx
import { getChatViewEntityBinding } from "stream-chat-react/slot-layout";
import type { ResolveDuplicateEntity } from "stream-chat-react/slot-layout";

export const resolveDuplicateEntity: ResolveDuplicateEntity = ({ binding }) =>
  getChatViewEntityBinding(binding)?.kind === "channel" ? "move" : "allow";
```

A channel or thread stacked as a layer isn't checked.

### viewActionSlotResolvers

`viewActionSlotResolvers` takes, per view, a `ViewActionSlotResolvers`: `{ openChannel?, openThread? }`. Each is a `ResolveViewActionTargetSlot`, which returns a slot name, or `undefined` to leave the choice to `ChatView`. It receives `ResolveViewActionTargetSlotArgs`:

| Field            | Type                                             | Description                                                           |
| ---------------- | ------------------------------------------------ | --------------------------------------------------------------------- |
| `action`         | `"openChannel" \| "openThread"`                  | What's being opened.                                                  |
| `activeView`     | `ChatView`                                       | The view it opens in.                                                 |
| `availableSlots` | `string[]`                                       | The view's slots that can be opened into.                             |
| `slotNames`      | `string[] \| undefined`                          | The view's declared slots, or `availableSlots` when it declares none. |
| `slotBindings`   | `Record<string, LayoutSlotBinding \| undefined>` | What each slot holds.                                                 |
| `requestedSlot`  | `string \| undefined`                            | The `slot` the open asked for.                                        |

A resolver isn't asked for an open with `additive`, and a slot name the view doesn't have is ignored. [Changing navigation behaviour](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#changing-navigation-behaviour) has an example.

### deriveWorkspaceNavigation

`deriveWorkspaceNavigation` is a `DeriveWorkspaceNavigation`. See [Overriding navigation](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/workspace-navigation-api/#overriding-navigation).

## Reading layout state

These hooks read the layout and re-render when it changes. They throw outside a `ChatView`. Without `view`, they read the shown view.

| Hook                                            | Returns                                                                                                   |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `useLayoutViewState(view?)`                     | The view's [`ChatViewLayoutViewState`](#chatviewlayoutviewstate).                                         |
| `useSlotEntity({ kind, slot?, view? })`         | The `source` of the first slot that holds `kind`, or `undefined`. With `slot`, only that slot is checked. |
| `useSlotEntities({ kind, slot?, view? })`       | Every slot that holds `kind`, as `{ slot, source }`.                                                      |
| `useSlotTopLayerEntity({ kind, slot?, view? })` | The `source` of the first slot whose top layer is of `kind`, or `undefined`.                              |
| `useSlotChannel({ slot?, view? })`              | `useSlotEntity` for `channel`. The options are optional.                                                  |
| `useSlotChannels({ slot?, view? })`             | Every slot that holds a channel, as `{ channel, slot }`.                                                  |
| `useSlotThread({ slot?, view? })`               | `useSlotEntity` for `thread`.                                                                             |
| `useSlotThreads({ slot?, view? })`              | Every slot that holds a thread, as `{ slot, thread }`.                                                    |
| `useSlotForKind(kind)`                          | The first slot of the shown view that holds `kind`.                                                       |
| `useSlotForKey(key)`                            | The slot of the shown view that holds the channel or thread with `key`: a `cid` or a thread id.           |

The entity hooks read what a slot holds underneath any layer, except `useSlotTopLayerEntity`. Pass `slot` to be precise: without it, they return the first match, which is only right in a layout with one of each.

```tsx
import { useSlotEntity, useSlotThreads } from "stream-chat-react/slot-layout";

export const WorkspaceStatus = () => {
  const channel = useSlotEntity({ kind: "channel", slot: "channel" });
  const threads = useSlotThreads();

  return (
    <p>
      {channel ? channel.cid : "No channel"}, {threads.length} threads open
    </p>
  );
};
```

`getLayoutViewState(state, view?)` reads the same without a hook, for example from `layoutController.state.getLatestValue()`.

### ChatViewLayoutViewState

| Field                | Type                                                   | Description                                                  |
| -------------------- | ------------------------------------------------------ | ------------------------------------------------------------ |
| `availableSlots`     | `string[]`                                             | The slots navigation can open into, in order.                |
| `slotNames`          | `string[] \| undefined`                                | The view's declared slots.                                   |
| `slotBindings`       | `Record<string, LayoutSlotBinding \| undefined>`       | What each slot holds.                                        |
| `slotLayers`         | `Record<string, LayoutSlotBinding[] \| undefined>`     | The layers on each slot, bottom first. Optional in the type. |
| `hiddenSlots`        | `Record<string, boolean \| undefined>`                 | Which slots are hidden.                                      |
| `slotHistory`        | `Record<string, LayoutSlotBinding[] \| undefined>`     | Each slot's back list, oldest first.                         |
| `slotForwardHistory` | `Record<string, LayoutSlotBinding[] \| undefined>`     | Each slot's forward list.                                    |
| `slotMeta`           | `Record<string, { occupiedAt?: number } \| undefined>` | When each slot was filled, as a timestamp in milliseconds.   |

### Saving and restoring

| Function                                                             | Returns                                                                                                                 |
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `serializeLayoutState(state, { serializeEntityBinding? })`           | A `ChatViewLayoutSnapshot`: the shown view, and each view's slots, what they hold, which are hidden, and their history. |
| `restoreLayoutState(snapshot, { deserializeEntityBinding? })`        | The `ChatViewLayoutState` to set on the layout controller's `state`.                                                    |
| `serializeLayoutControllerState(layoutController, options?)`         | `serializeLayoutState` of the controller's current state.                                                               |
| `restoreLayoutControllerState(layoutController, snapshot, options?)` | Nothing: sets the restored state on the controller.                                                                     |

- `serializeEntityBinding` is a `SerializeLayoutEntityBinding`: `(binding: LayoutSlotBinding) => SerializedLayoutSlotBinding | undefined`. It turns each binding into something JSON can hold, such as `{ key, payload: { cid } }`. Return `undefined` to leave a binding out. Without it, bindings are copied as they are, channel and thread objects included, so the snapshot can't be stored as JSON.
- `deserializeEntityBinding` is a `DeserializeLayoutEntityBinding`: `(saved: SerializedLayoutSlotBinding) => LayoutSlotBinding | undefined`. It turns each saved entry back into a binding. Without it, saved entries are used as they are.
- Layers aren't saved.

[Saving and restoring a layout](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#saving-and-restoring-a-layout) has a complete example.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/slot-layout/](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/slot-layout/).