# Navigation

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

A row in the channel list doesn't know where channels open. When the user clicks it, the row calls `openChannel(channel)` and leaves the rest to the layout. Every SDK component that opens, closes or highlights a channel or a thread works the same way, through one interface: `useWorkspaceNavigation()`.

The interface is part of `stream-chat-react`. The layout that implements it, `ChatView`, is in `stream-chat-react/slot-layout`. [Moving between channels, threads and messages](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/moving-between-channels-threads-and-messages/) lists every component that uses it, and [Workspace with ChatView](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/) explains where `ChatView` puts what they open.

## One navigation interface

`useWorkspaceNavigation()` returns the same object anywhere below the layout. Its members do three things:

- **Open:** `openChannel(channel, options?)` and `openThread(target, options?)`. The target of `openThread` is a `Thread`, or `{ channel, message }` for the thread of that parent message.
- **Close and go back:** `closeChannel(cid?, options?)` and `closeThread(threadId?, options?)` close a panel. `goBack(key?, options?)` removes a channel or thread that was opened on top of something else, and `canGoBack(key?, panel?)` says whether there is anything beneath it.
- **Say what's open:** `openChannels` and `openThreads` list what the panels hold. `isChannelActive(cid?)` and `isThreadActive(threadId?)` say whether a channel or thread is on screen: open in a panel and not covered by something opened on top of it. `isChannelDismissable` and `isThreadDismissable` say whether its panel can be closed, and `isThreadsView` whether the thread list's view is the one shown.

For example, this button opens a support channel and shows when it's on screen:

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

export const SupportButton = ({ channel }: { channel: StreamChannel }) => {
  const { isChannelActive, openChannel } = useWorkspaceNavigation();

  return (
    <button
      aria-pressed={isChannelActive(channel.cid)}
      onClick={(event) => openChannel(channel, { event })}
      type="button"
    >
      Support
    </button>
  );
};
```

`openChannel` places a channel; it doesn't load it. A channel from a channel list is loaded by the list's query. A channel you get from `client.channel()` needs `Channel.watch()` first, as in [Show one channel](https://getstream.io/chat/docs/sdk/react/v15/getting-started/quick-start/#show-one-channel).

## Beside and on top

Every operation takes the same options:

- `additive: true` opens **beside** the current content instead of replacing it. In a layout with a `channel` slot and a `thread` slot, a channel opened this way goes into the `thread` slot, next to the channel that's already open. A layout with one slot ignores it.
- `layer: true` opens **on top** of what the panel shows. Going back removes it and shows what's beneath. The panel'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.
- `event` is the click or key press that started the navigation. The channel list, search results and header buttons pass it, so an override can read the ⌘ or ctrl key. The SDK doesn't act on it itself.
- `panel` is the panel the call comes from. See [Panels](#panels) below.

The two can happen together. When the slot beside the channel already shows a thread, `ChatView` stacks a channel opened beside it on top of that thread. The back button in that channel's header returns to the thread. [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 the full rules.

What shows depends on how the layout renders the slot. `Slot` shows whatever the slot holds, layers included. `ChannelSlot` shows only a channel and `ThreadSlot` only a thread, without layers. So in the Quick start app, a channel opened beside or on top doesn't show. See [Rendering a slot](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#rendering-a-slot).

The thread list opens a thread beside the current one on ⌘/ctrl-click, when its view has a second slot for it. The channel list passes the event but always replaces the open channel; [⌘/ctrl-click opens beside](https://getstream.io/chat/docs/sdk/react/v15/recipes/layout/ctrl-click-opens-beside/) changes that.

### Panels

A panel is one slot of the layout. The same channel can be open in two panels at once, for example after the user opens it beside itself. `closeChannel(cid)` then has to know which of the two to close.

`ChannelSlot`, `ThreadSlot` and `Slot` declare the panel they render through `WorkspacePanelProvider`, and `useWorkspacePanel()` reads it. The back and close buttons in `ChannelHeader` and `ThreadHeader` pass it as `panel`, so each acts on its own panel. A control of your own does the same:

```tsx
import {
  useChannel,
  useWorkspaceNavigation,
  useWorkspacePanel,
} from "stream-chat-react";

export const CloseChannelButton = () => {
  const channel = useChannel();
  const panel = useWorkspacePanel();
  const { closeChannel, isChannelDismissable } = useWorkspaceNavigation();

  // Only a channel opened beside another one can be closed.
  if (!isChannelDismissable(channel.cid, panel)) return null;

  return (
    <button
      onClick={(event) => closeChannel(channel.cid, { event, panel })}
      type="button"
    >
      Close
    </button>
  );
};
```

The SDK's own buttons are `WorkspaceNavigationBackButton` and `WorkspaceNavigationCloseButton`. A custom header can render them to keep them.

## Without a layout

Outside `ChatView` and any `WorkspaceNavigationProvider`, `useWorkspaceNavigation()` returns a default that does nothing. Every open and close is ignored, `openChannels` and `openThreads` are empty, and every check returns `false`. Components call it without checking for a layout, so they still render:

- A row in the channel list does nothing when clicked, unless it has an `onSelect` handler.
- The replies button and the thread actions in the message menu do nothing.
- No row in the channel list is highlighted as open.
- The back and close buttons in the headers render nothing.
- Selecting a pinned message in channel details still jumps to the message in the channel, and **Send direct message** on a member's profile still creates the direct message channel, but neither opens the channel.

The first time each of `openChannel`, `openThread`, `closeChannel`, `closeThread` and `goBack` is called on the default, it logs a console warning that no workspace navigation is provided. The `defaultWorkspaceNavigation` export doesn't warn, so the members you leave out when you spread it into your own navigation stay silent.

An app that shows one channel needs none of this: it passes the channel to `Channel` itself, as in the [Quick start](https://getstream.io/chat/docs/sdk/react/v15/getting-started/quick-start/#show-one-channel). For navigation to do something, render [`ChatView`](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/), or provide your own.

## Providing your own navigation

`WorkspaceNavigationProvider` gives the components below it a navigation of your own. This app keeps the open channel and thread in React state, without `ChatView`:

```tsx
import { useMemo, useState } from "react";
import { Thread as StreamThread } from "stream-chat";
import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  ChannelHeader,
  ChannelNavigation,
  defaultWorkspaceNavigation,
  MessageComposer,
  MessageList,
  Thread,
  ThreadHeader,
  useChatContext,
  WorkspaceNavigationProvider,
} from "stream-chat-react";
import type { WorkspaceNavigation } from "stream-chat-react";
import { useActiveThread } from "stream-chat-react/slot-layout";

export const TwoPaneChat = () => {
  const { client } = useChatContext();
  const [channel, setChannel] = useState<StreamChannel>();
  const [thread, setThread] = useState<StreamThread>();

  // Marks the thread as on screen, so its new replies are marked read.
  useActiveThread({ activeThread: thread });

  const navigation = useMemo<WorkspaceNavigation>(
    () => ({
      // The members left out keep the defaults, which do nothing.
      ...defaultWorkspaceNavigation,
      closeThread: () => setThread(undefined),
      isChannelActive: (cid) => cid !== undefined && cid === channel?.cid,
      isThreadActive: (threadId) =>
        threadId !== undefined && threadId === thread?.id,
      isThreadDismissable: () => true,
      openChannel: (nextChannel) => {
        setChannel(nextChannel);
        setThread(undefined);
      },
      openChannels: channel ? [channel] : [],
      openThread: (target) =>
        setThread(
          target instanceof StreamThread
            ? target
            : client.threads.ensure({
                channel: target.channel,
                parentMessage: target.message,
              }),
        ),
      openThreads: thread ? [thread] : [],
    }),
    [channel, client, thread],
  );

  return (
    <WorkspaceNavigationProvider value={navigation}>
      <ChannelNavigation />
      {channel && (
        <Channel channel={channel}>
          <ChannelHeader />
          <MessageList />
          <MessageComposer />
        </Channel>
      )}
      {thread && (
        <Thread thread={thread}>
          <ThreadHeader />
          <MessageList />
          <MessageComposer />
        </Thread>
      )}
    </WorkspaceNavigationProvider>
  );
};
```

Here is what each part does:

1. A click on a row in the channel list calls `openChannel`, which shows the channel and closes the thread.
2. The replies button calls `openThread` with the channel and the parent message. `client.threads.ensure()` returns the thread for that message, creating it if it doesn't exist yet.
3. `isThreadDismissable` returns `true`, so `ThreadHeader` shows its close button, which calls `closeThread`.
4. `isChannelActive` highlights the open channel's row.

Memoize the value: every component that reads it re-renders when it changes. `useActiveThread` comes from the layout's entry point but doesn't need `ChatView`.

To change how `ChatView` navigates rather than replace it, pass `deriveWorkspaceNavigation` to `ChatView`. See [Changing navigation behaviour](https://getstream.io/chat/docs/sdk/react/v15/ui/layout/workspace-with-chatview/#changing-navigation-behaviour).

## Used in

- [Channel lists](https://getstream.io/chat/docs/sdk/react/v15/features/channels/channel-lists/)
- [Search](https://getstream.io/chat/docs/sdk/react/v15/features/channels/search/)
- [Replies and threads](https://getstream.io/chat/docs/sdk/react/v15/features/messaging/replies-and-threads/)
- [⌘/ctrl-click opens beside](https://getstream.io/chat/docs/sdk/react/v15/recipes/layout/ctrl-click-opens-beside/)

The members, their types and the options are listed in [Workspace navigation API](https://getstream.io/chat/docs/sdk/react/v15/component-reference/layout/workspace-navigation-api/).

---

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