# Customizing ChatView

`<ChatView/>` works without configuration beyond its views and slots, set up as in [Workspace with ChatView](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/). Use this page to change what it does: how navigation behaves, what happens when something opens on top of a panel, going back and forward in a slot, taking a panel off screen, and saving the layout so a reload restores it. It builds on the open options in [Beside and on top](https://getstream.io/chat/docs/sdk/react/ui/layout/navigation/#beside-and-on-top). The props are in [ChatView components](https://getstream.io/chat/docs/sdk/react/component-reference/layout/chatview-components/) and [Slot layout](https://getstream.io/chat/docs/sdk/react/component-reference/layout/slot-layout/).

## Changing navigation behaviour

`deriveWorkspaceNavigation` changes what the navigation of the components inside `<ChatView/>` does. It receives the SDK's navigation and returns the members to replace. The members it leaves out keep the SDK's behaviour. Here is a `deriveWorkspaceNavigation` function that opens a channel beside the open one on ⌘/ctrl-click:

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

// Declared outside the component, so ChatView gets the same function on every render.
export const deriveWorkspaceNavigation: DeriveWorkspaceNavigation = (base) => ({
  openChannel: (channel, options) =>
    base.openChannel(channel, {
      ...options,
      additive:
        options?.additive ??
        !!(options?.event?.metaKey || options?.event?.ctrlKey),
    }),
});
```

Pass it as `<ChatView deriveWorkspaceNavigation={deriveWorkspaceNavigation}>`. Keep it stable: declare it outside the component, or wrap it in `useCallback`. A new function on every render rebuilds the navigation on every render. [⌘/ctrl-click opens beside](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/ctrl-click-opens-beside/) walks through this example.

The override reaches every component that uses `useWorkspaceNavigation()`, selecting a pinned message and **Send direct message** in channel details included. Those two pass no event, so this override opens their channel in place of the open one.

Two more props change where things go:

- **`viewActionSlotResolvers`** picks the slot for a channel or a thread, per view. A resolver returns a slot name, or `undefined` to leave the choice to `ChatView`. It isn't asked for opens beside, with `additive`. It may return a slot of the layout that the view doesn't show yet, which adds it. `<ChatView/>` reads the resolvers after its first render and stores them, so keep them stable: a new object on every render makes it store them again each time.

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

  // In the channels view, threads always open in the "thread" slot.
  export const viewActionSlotResolvers: ChatViewProps["viewActionSlotResolvers"] =
    {
      channels: { openThread: () => "thread" },
    };
  ```

- **`duplicateEntityPolicy`** says what happens when a channel or thread is opened into a slot while another slot holds it, for example when the user opens the open channel beside itself. With `allow`, the default, both slots show it. With `move`, the other slot empties. With `reject`, nothing opens, including when the channel is already open in the slot it's opened into. `resolveDuplicateEntity` decides per open instead, and returns one of the three. A channel or thread stacked as a layer isn't checked.

## Layers and history

A slot can hold more than one thing over time, in two ways: layers on top of each other, and a history of what it held before.

### Layers

A layer is something opened on top of a slot's content. The slot's own content mounts again when the first layer opens and when the last one closes, so it loses its scroll position and the state kept in its components. From the second layer up, everything beneath the top layer stays mounted.

For example, take the side-by-side app from [`<Slot/>` and `slotRenderers`](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/#slot-and-slotrenderers), with the ⌘/ctrl-click override from [Changing navigation behaviour](#changing-navigation-behaviour). "Design" is open on the left, and a thread from it on the right. The user ⌘-clicks "Support". It opens on the right, on top of the thread. "Support"'s header now has a back button. Back removes "Support", and the thread shows again. "Support" was the only layer, so the thread mounts again and doesn't keep its scroll position. Close closes the whole right panel: "Support" and the thread beneath it.

Layers come from three places:

- an open into a slot in use other than the main panel, as described in [Where an opened channel or thread lands](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/#where-an-opened-channel-or-thread-lands);
- the `layer: true` option;
- `pushLayer(slot, binding)` from `useChatViewNavigation()`, which stacks anything on a slot.

Here is a `useOpenProfile` hook that opens a member's profile on top of the thread panel:

```tsx
import { useChatViewNavigation } from "stream-chat-react/slot-layout";

export const useOpenProfile = () => {
  const { pushLayer } = useChatViewNavigation();

  return (userId: string) =>
    pushLayer("thread", {
      key: userId,
      kind: "userProfile",
      source: { userId },
    });
};
```

The SDK's renderer for `userProfile` shows only the user's id. Render your own through `slotRenderers.userProfile`.

`close(slot)` and `popLayer(slot)` remove the top layer. Only `<Slot/>` renders layers; see [Rendering a slot](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/#rendering-a-slot).

### Back and forward per slot

Each slot remembers what it held before. When an open replaces what a slot holds, the old content goes onto that slot's back list. Going back shows it again and keeps the current content for going forward. Opening something new clears the forward list, and emptying the slot clears both.

The SDK's back button doesn't use this history: it removes a layer. For buttons of your own, call `LayoutController.goBack(slot)` and `LayoutController.goForward(slot)` on `<ChatView/>`'s layout controller. Here is a `ChannelHistoryButtons` component with a previous and a next button for the `channel` slot:

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

export const ChannelHistoryButtons = () => {
  const { layoutController } = useChatViewContext();
  const { slotForwardHistory, slotHistory } = useLayoutViewState();

  return (
    <>
      <button
        disabled={!slotHistory.channel?.length}
        onClick={() => layoutController.goBack("channel")}
        type="button"
      >
        Previous channel
      </button>
      <button
        disabled={!slotForwardHistory.channel?.length}
        onClick={() => layoutController.goForward("channel")}
        type="button"
      >
        Next channel
      </button>
    </>
  );
};
```

`useLayoutViewState()` reads the shown view's slots, what they hold, their layers and their history.

### Hiding vs closing

`useChatViewNavigation()` has three ways to take something off screen:

| Call            | What it does                                                                        |
| --------------- | ----------------------------------------------------------------------------------- |
| `hide(slot)`    | Stops showing the slot. What it holds stays mounted. `unhide(slot)` shows it again. |
| `close(slot)`   | Removes the top layer. When the slot has no layers, it empties the slot.            |
| `release(slot)` | Empties the slot: what it holds, every layer on it, and its history.                |

A hidden `<Slot/>` gets the `str-chat__chat-view__slot--hidden` class, which hides it, and `aria-hidden`. `<ChannelSlot/>` and `<ThreadSlot/>` don't read the hidden flag. `<ThreadSlot/>` hides its slot while it holds no thread, unless you pass `hideIfEmpty={false}`; that matters when a `<Slot/>` is around it.

The close button in a header uses `close` in the main panel and `release` in the panels beside it. The back button uses `close`.

### List slots

A slot that holds a `channelList` or a `threadList` is a list slot. Navigation never picks it on its own for what it opens, and `close` and `release` never empty it. To take a list off screen, hide its slot.

For example, a one-column layout built on the [built-in workspace layout](https://getstream.io/chat/docs/sdk/react/ui/layout/workspace-with-chatview/#the-built-in-workspace-layout) can hide the list while a channel is open. Here is a `HideListWhileChannelOpen` component that does that:

```tsx
import { useEffect } from "react";
import {
  useChatViewContext,
  useSlotChannel,
} from "stream-chat-react/slot-layout";

export const HideListWhileChannelOpen = () => {
  const { layoutController } = useChatViewContext();
  const channel = useSlotChannel({ slot: "channel" });

  useEffect(() => {
    if (channel) layoutController.hide("list");
    else layoutController.unhide("list");
  }, [channel, layoutController]);

  return null;
};
```

It calls the layout controller rather than `useChatViewNavigation()`. The navigation's functions change with every layout change, so an effect that depends on them would run again after every change it makes.

## Saving and restoring a layout

A layout can be saved, for example to `localStorage`, and restored when the app loads.

### Saving

`serializeLayoutState(state, options)` turns the layout's state into a snapshot: the shown view, and for each view its slots, what they hold, which are hidden, and their history.

What a slot holds is the channel or thread object itself, which can't be stored as JSON. Pass `serializeEntityBinding` to turn each one into something that can, such as the channel's `cid`. Return `undefined` to leave one out.

### Restoring

`restoreLayoutState(snapshot, options)` turns a snapshot back into the layout's state. Its `deserializeEntityBinding` turns each saved entry back into a channel or thread. Set the result on the layout controller. It replaces the whole layout state, including the shown view and each view's slots, so a snapshot saved under different `layouts` brings back the old slots.

Here is a `LayoutPersistence` component that saves the open channels on every change, and restores them when it mounts. Render it inside `<ChatView/>`:

```tsx
import { useEffect } from "react";
import type { StreamChat } from "stream-chat";
import { useChatContext } from "stream-chat-react";
import {
  createChatViewSlotBinding,
  getChatViewEntityBinding,
  restoreLayoutState,
  serializeLayoutState,
  useChatViewContext,
} from "stream-chat-react/slot-layout";
import type {
  ChatViewLayoutSnapshot,
  DeserializeLayoutEntityBinding,
  SerializeLayoutEntityBinding,
} from "stream-chat-react/slot-layout";

const STORAGE_KEY = "chat-layout";

// A channel is saved as its cid. Everything else, such as threads, is left out.
const serializeEntityBinding: SerializeLayoutEntityBinding = (binding) => {
  const entity = getChatViewEntityBinding(binding);
  if (entity?.kind !== "channel") return undefined;
  return { key: binding.key, payload: { cid: entity.source.cid } };
};

const createDeserializer =
  (client: StreamChat): DeserializeLayoutEntityBinding =>
  (saved) => {
    const { cid } = saved.payload as { cid: string };
    const [type, id] = cid.split(":");
    const channel = client.channel(type, id);
    // A restored channel isn't loaded yet; watching loads it.
    channel
      .watch()
      .catch((error) => console.error("Could not watch the channel", error));
    return createChatViewSlotBinding({
      key: cid,
      kind: "channel",
      source: channel,
    });
  };

export const LayoutPersistence = () => {
  const { client } = useChatContext();
  const { layoutController } = useChatViewContext();

  useEffect(() => {
    const saved = localStorage.getItem(STORAGE_KEY);
    if (saved) {
      const snapshot = JSON.parse(saved) as ChatViewLayoutSnapshot;
      layoutController.state.next(
        restoreLayoutState(snapshot, {
          deserializeEntityBinding: createDeserializer(client),
        }),
      );
    }

    return layoutController.state.subscribe((state) => {
      const snapshot = serializeLayoutState(state, { serializeEntityBinding });
      localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
    });
  }, [client, layoutController]);

  return null;
};
```

`serializeLayoutControllerState(layoutController, options)` and `restoreLayoutControllerState(layoutController, snapshot, options)` do the same in one call each.

`restoreLayoutState` calls the deserializer for every saved entry, each slot's back and forward history included, so this one watches every saved channel, not only the ones on screen. Watching is get-or-create: a saved channel that has since been deleted is created again. [Saving and restoring the layout](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/saving-and-restoring-the-layout/) watches each channel when it's shown instead, and also keeps the open channel's `cid` in the URL.

### What isn't saved

- **Layers.** A reload restores what each slot holds underneath, never something opened on top of it.
- **What your serializer leaves out.** Above, threads aren't saved, so a restored channel opens without its thread. In a layout with list slots, save the lists too, or a restored list slot is empty. A list binding holds no data, so it can be saved as it is.
- **Loaded data.** A snapshot names channels; it doesn't hold their messages. Each restored channel loads again.

## Your own layout controller

`<ChatView/>` keeps the layout's state in a layout controller, which it creates from `layouts`, `duplicateEntityPolicy` and `resolveDuplicateEntity`. When one of those changes, it creates a new controller, and everything open closes. Pass `layoutController` to give it a controller of your own instead: `<ChatView/>` uses it in place of the one it would create, so the layout's state lives as long as your controller does.

The SDK exports the controller's type, `LayoutController`, but not the class that implements it, so a controller of your own implements that type. To work with the controller `<ChatView/>` created, read it with `useChatViewContext().layoutController`, as the examples on this page do.

## Used in

- [⌘/ctrl-click opens beside](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/ctrl-click-opens-beside/)
- [Single-pane mobile with back button](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/single-pane-mobile-with-back-button/)
- [Saving and restoring the layout](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/layout/saving-and-restoring-the-layout/)

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/ui/layout/customizing-chatview/](https://getstream.io/chat/docs/sdk/react/ui/layout/customizing-chatview/).