# Navigation

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

The SDK doesn't navigate. Its components tell your app where the user wants to go, and your navigator shows the screen. A row in `ChannelList` calls `onSelect` with the channel, the replies button calls `onThreadSelect` with the thread, and your handler pushes the channel screen or the thread screen. React Native apps navigate with [React Navigation](https://reactnavigation.org/) or [Expo Router](https://docs.expo.dev/router/introduction/), and this page shows both.

Every callback the SDK uses to move the user is listed on [Moving between channels, threads and messages](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/moving-between-channels-threads-and-messages/). How the screens avoid the keyboard and the navigation header is on [Keyboard and insets](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/keyboard-and-insets/).

## Where the providers go

The providers go around the navigator, in this order:

1. `SafeAreaProvider`, because the composer, the message menu and the image gallery read the safe area insets.
2. `GestureHandlerRootView`, for the gestures of the message list, the message menu and the bottom sheets.
3. `OverlayProvider`, which hosts the message menu and the image gallery over everything it wraps.
4. `Chat`, which gives the client, the translations and the theme to the SDK components below it.
5. The navigator, which is `NavigationContainer` with your navigators, or, with Expo Router, the `Stack` in `app/_layout.tsx`.

`OverlayProvider` has to wrap the navigator, so the message menu and the image gallery draw over every screen, header and tab bar. It works on either side of `NavigationContainer`, which renders no view of its own. The difference is which providers the components that `OverlayProvider` renders itself can read: the image gallery, and a custom `MessageActions` from `WithComponents`. Above `NavigationContainer`, a custom gallery component or `MessageActions` can't call React Navigation hooks such as `useNavigation()`. Put `OverlayProvider` inside `NavigationContainer` when one of them needs to. With Expo Router, the root layout is already inside the router's navigation container.

Mount one `OverlayProvider`. A screen presented in a separate native window, such as a native modal on iOS, needs one of its own, as described in [Chat in modals](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/chat-in-modals/). Never mount one for each screen or tab.

Keep `Chat` mounted while the user is connected. While it's mounted, it keeps the event subscriptions of the thread list, polls and reminders, and closes and reopens the WebSocket as the app goes to the background and comes back. See [How components connect to state](https://getstream.io/chat/docs/sdk/react-native/v10/ui/how-components-connect-to-state/#overlayprovider-and-chat).

## A channel list, a channel and a thread

The three screens are the same with both navigators:

- The channel list screen renders `ChannelList` and navigates with the channel's `cid` in `onSelect`.
- The channel screen looks the channel up by its `cid` and renders it in `Channel`, with `MessageList` and `MessageComposer`. `onThreadSelect` on `MessageList` navigates with the thread's id, which is the id of its parent message.
- The thread screen looks the thread up by its id and renders it in `Thread`, with `MessageList` and `MessageComposer`. `Thread` goes on a screen of its own, not inside `Channel`.

Both screens pass the height of the navigation header to `keyboardVerticalOffset` and `topInset`, because a native header sits above the screen's views. See [Keyboard and insets](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/keyboard-and-insets/).

**React Navigation**

This app uses the native stack, as the [Quick start](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/quick-start/#add-threads) does:

```tsx title="App.tsx"
import { useEffect, useMemo } from "react";
import { StyleSheet, Text } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { useHeaderHeight } from "@react-navigation/elements";
import { NavigationContainer } from "@react-navigation/native";
import {
  createNativeStackNavigator,
  type NativeStackScreenProps,
} from "@react-navigation/native-stack";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import {
  Channel,
  ChannelList,
  Chat,
  MessageComposer,
  MessageList,
  OverlayProvider,
  Thread,
  useChatContext,
  useCreateChatClient,
} from "stream-chat-react-native";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

const filters: ChannelFilters = {
  members: { $in: [userId] },
  type: "messaging",
};
const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];

type StackParamList = {
  ChannelList: undefined;
  Channel: { cid: string };
  Thread: { threadId: string };
};

const Stack = createNativeStackNavigator<StackParamList>();

const ChannelListScreen = ({
  navigation,
}: NativeStackScreenProps<StackParamList, "ChannelList">) => {
  const { client } = useChatContext();
  const paginator = useMemo(
    () => new ChannelPaginator({ client, filters, id: "my-channels", sort }),
    [client],
  );

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

  return (
    <ChannelList
      onSelect={(channel) =>
        navigation.navigate("Channel", { cid: channel.cid })
      }
      paginator={paginator}
    />
  );
};

const ChannelScreen = ({
  navigation,
  route,
}: NativeStackScreenProps<StackParamList, "Channel">) => {
  const { client } = useChatContext();
  const headerHeight = useHeaderHeight();
  const channel = client.channelManager.get(route.params.cid);

  if (!channel) return null;

  return (
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      topInset={headerHeight}
    >
      <MessageList
        onThreadSelect={(thread) =>
          navigation.navigate("Thread", { threadId: thread.id })
        }
      />
      <MessageComposer />
    </Channel>
  );
};

const ThreadScreen = ({
  route,
}: NativeStackScreenProps<StackParamList, "Thread">) => {
  const { client } = useChatContext();
  const headerHeight = useHeaderHeight();
  const thread = client.threads.get(route.params.threadId);

  if (!thread) return null;

  return (
    <Thread
      keyboardVerticalOffset={headerHeight}
      thread={thread}
      topInset={headerHeight}
    >
      <MessageList />
      <MessageComposer />
    </Thread>
  );
};

const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  return (
    <SafeAreaProvider>
      <GestureHandlerRootView style={styles.container}>
        {client ? (
          <OverlayProvider>
            <Chat client={client}>
              <NavigationContainer>
                <Stack.Navigator>
                  <Stack.Screen
                    component={ChannelListScreen}
                    name="ChannelList"
                  />
                  <Stack.Screen component={ChannelScreen} name="Channel" />
                  <Stack.Screen component={ThreadScreen} name="Thread" />
                </Stack.Navigator>
              </NavigationContainer>
            </Chat>
          </OverlayProvider>
        ) : (
          <Text>Connecting...</Text>
        )}
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 },
});

export default App;
```

**Expo Router**

Expo Router builds the stack from the files in `app/`. The root layout creates the client and renders the providers around the `Stack`:

```tsx title="app/_layout.tsx"
import { StyleSheet, Text } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Stack } from "expo-router";
import { Chat, OverlayProvider, useCreateChatClient } from "stream-chat-expo";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

export default function RootLayout() {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  return (
    <SafeAreaProvider>
      <GestureHandlerRootView style={styles.container}>
        {client ? (
          <OverlayProvider>
            <Chat client={client}>
              <Stack />
            </Chat>
          </OverlayProvider>
        ) : (
          <Text>Connecting...</Text>
        )}
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
});
```

The channel list is the first screen, `app/index.tsx`:

```tsx title="app/index.tsx"
import { useEffect, useMemo } from "react";
import { Stack, useRouter } from "expo-router";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import { ChannelList, useChatContext } from "stream-chat-expo";

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

export default function ChannelListScreen() {
  const router = useRouter();
  const { client } = useChatContext();

  const paginator = useMemo(() => {
    const filters: ChannelFilters = {
      members: { $in: [client.userId ?? ""] },
      type: "messaging",
    };
    return new ChannelPaginator({ client, filters, id: "my-channels", sort });
  }, [client]);

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

  return (
    <>
      <Stack.Screen options={{ title: "Channels" }} />
      <ChannelList
        onSelect={(channel) =>
          router.push({
            params: { cid: channel.cid },
            pathname: "/channel/[cid]",
          })
        }
        paginator={paginator}
      />
    </>
  );
}
```

The channel screen, `app/channel/[cid].tsx`, reads the `cid` from the route:

```tsx title="app/channel/[cid].tsx"
import { useHeaderHeight } from "@react-navigation/elements";
import { useLocalSearchParams, useRouter } from "expo-router";
import {
  Channel,
  MessageComposer,
  MessageList,
  useChatContext,
} from "stream-chat-expo";

export default function ChannelScreen() {
  const { cid } = useLocalSearchParams<{ cid: string }>();
  const router = useRouter();
  const { client } = useChatContext();
  const headerHeight = useHeaderHeight();
  const channel = client.channelManager.get(cid);

  if (!channel) return null;

  return (
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      topInset={headerHeight}
    >
      <MessageList
        onThreadSelect={(thread) =>
          router.push({
            params: { threadId: thread.id },
            pathname: "/thread/[threadId]",
          })
        }
      />
      <MessageComposer />
    </Channel>
  );
}
```

The thread screen is `app/thread/[threadId].tsx`:

```tsx title="app/thread/[threadId].tsx"
import { useHeaderHeight } from "@react-navigation/elements";
import { useLocalSearchParams } from "expo-router";
import {
  MessageComposer,
  MessageList,
  Thread,
  useChatContext,
} from "stream-chat-expo";

export default function ThreadScreen() {
  const { threadId } = useLocalSearchParams<{ threadId: string }>();
  const { client } = useChatContext();
  const headerHeight = useHeaderHeight();
  const thread = client.threads.get(threadId);

  if (!thread) return null;

  return (
    <Thread
      keyboardVerticalOffset={headerHeight}
      thread={thread}
      topInset={headerHeight}
    >
      <MessageList />
      <MessageComposer />
    </Thread>
  );
}
```

`useHeaderHeight` comes from `@react-navigation/elements`, which Expo Router uses. Versions of `expo-router` that bundle React Navigation export it from `expo-router/react-navigation` instead, as the `ExpoMessaging` example app imports it.

The channel list stays mounted under the screens pushed on top of it, so it keeps its paginator and its scroll position. When the user goes back, `Channel` and `Thread` unmount. The channel and the thread stay in the client: the next time the user opens them, they show what is already loaded, and `Channel` loads the latest messages only when the loaded ones don't reach them.

## Passing channels and threads between screens

A screen needs the `Channel` or `Thread` object to render. There are two ways to hand it over.

### Ids in the route params

The examples above pass the channel's `cid` and the thread's id, and the next screen looks the object up in the client:

- `client.channelManager.get(cid)` returns the channel the client keeps for that `cid`, or `undefined` when it keeps none. A channel from a channel list is always there while the list holds it.
- `client.channel(type, id)` returns the same stored channel, or creates and stores one. `Channel` watches a new one when it mounts, so a screen that can open before any list loaded its channel, from a push notification or a deep link, uses it.
- `client.threads.get(threadId)` returns the thread the client keeps for that id: one in the thread list, or one opened during the session. `onThreadSelect` passes a thread the client keeps, so it's there when the thread screen renders.
- `client.getThreadAndHydrate({ message_id: threadId })` loads a thread from the server, for a thread screen that opens cold.

Params with ids can be saved and restored by the navigator, and the same screens serve deep links. These hooks resolve the objects for screens that can open cold:

```tsx title="hooks/useChatEntities.ts"
import { useEffect, useMemo, useState } from "react";
import type { Thread } from "stream-chat";
import { useChatContext } from "stream-chat-react-native";

export const useChannelByCid = (cid: string) => {
  const { client } = useChatContext();

  return useMemo(() => {
    // A cid is "type:id", and a channel type can't contain ":".
    const separator = cid.indexOf(":");
    return client.channel(cid.slice(0, separator), cid.slice(separator + 1));
  }, [client, cid]);
};

export const useThreadById = (threadId: string) => {
  const { client } = useChatContext();
  const [loaded, setLoaded] = useState<Thread>();
  const kept = client.threads.get(threadId);

  useEffect(() => {
    if (kept) return;
    let cancelled = false;
    client.getThreadAndHydrate({ message_id: threadId }).then(
      (thread) => {
        if (!cancelled) setLoaded(thread);
      },
      (error: unknown) => console.warn("Loading the thread failed", error),
    );
    return () => {
      cancelled = true;
    };
  }, [client, kept, threadId]);

  return kept ?? (loaded?.id === threadId ? loaded : undefined);
};
```

`Thread` registers the thread it renders in `client.threads`, so once the loaded thread is on screen, `client.threads.get(threadId)` returns it.

A `cid` is the stable address of a channel, with one exception. A channel created from its members, such as `client.channel("messaging", { members })`, has a temporary `cid` until its first query returns the real one. Watch it before navigating, and pass the `cid` of the channel `Channel.watch()` resolves with:

```tsx
const channel = await client
  .channel("messaging", {
    members: [{ user_id: userId }, { user_id: otherUserId }],
  })
  .watch();

navigation.navigate("Channel", { cid: channel.cid });
```

### An app context holding the objects

The other way keeps the open channel and thread in React state above the navigator, and the screens read them from there. The `ExpoMessaging` example app does this in `context/AppContext.tsx`:

```tsx title="context/AppContext.tsx"
import {
  createContext,
  useContext,
  useState,
  type PropsWithChildren,
} from "react";
import type { Channel, Thread } from "stream-chat";

type AppContextValue = {
  channel?: Channel;
  setChannel: (channel?: Channel) => void;
  setThread: (thread?: Thread) => void;
  thread?: Thread;
};

const AppContext = createContext<AppContextValue>({
  setChannel: () => undefined,
  setThread: () => undefined,
});

export const AppProvider = ({ children }: PropsWithChildren) => {
  const [channel, setChannel] = useState<Channel>();
  const [thread, setThread] = useState<Thread>();

  return (
    <AppContext.Provider value={{ channel, setChannel, setThread, thread }}>
      {children}
    </AppContext.Provider>
  );
};

export const useAppContext = () => useContext(AppContext);
```

The list sets the channel and navigates, `onThreadSelect` sets the thread and navigates, and the thread screen clears it with `onThreadDismount`, which `Thread` calls when it unmounts:

```tsx title="screens/ThreadScreen.tsx"
import { MessageComposer, MessageList, Thread } from "stream-chat-react-native";

import { useAppContext } from "../context/AppContext";

export const ThreadScreen = () => {
  const { setThread, thread } = useAppContext();

  if (!thread) return null;

  return (
    <Thread onThreadDismount={() => setThread(undefined)} thread={thread}>
      <MessageList />
      <MessageComposer />
    </Thread>
  );
};
```

A context holds one channel and one thread, so a stack with the same screen twice, such as a channel opened from another channel, shows the latest one on both. It's empty after the app restarts, so a screen opened from a deep link or from restored navigation state still needs the ids.

React Navigation accepts the objects themselves as route params, but warns that the params can't be serialized, and the navigation state then can't be saved or linked to.

## Where the channel list's paginator lives

`ChannelList` renders a `ChannelPaginator` that your app creates and registers with `client.channelManager.insertPaginator()`. It loads the first page when the paginator has none, and never disposes it. Where the paginator is created decides how long the loaded channels last:

- **In the list's screen**, as in the examples above. The paginator lives as long as the screen. That suits the first screen of a stack, which stays mounted under every screen pushed on it.
- **Above the navigator**, in a provider inside `Chat`. The paginator outlives the list's screen, so a list screen that unmounts and mounts again shows the same channels without querying again. The `ExpoMessaging` example app keeps its paginator in `AppContext`.

The channel manager moves channels between lists and reorders them as events arrive, but only while it's subscribed to the client's events. Each mounted `ChannelList` holds that subscription, so with no list on screen, a paginator above the navigator keeps its channels but stops following events. A provider that keeps lists for screens that unmount holds the subscription itself:

```tsx title="context/ChannelListsContext.tsx"
import {
  createContext,
  useContext,
  useEffect,
  useMemo,
  type PropsWithChildren,
} from "react";
import { ChannelPaginator } from "stream-chat";
import type { SortParamRequest } from "stream-chat";
import { useChatContext } from "stream-chat-react-native";

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

type ChannelLists = { archived: ChannelPaginator; inbox: ChannelPaginator };

const ChannelListsContext = createContext<ChannelLists | undefined>(undefined);

export const ChannelListsProvider = ({ children }: PropsWithChildren) => {
  const { client } = useChatContext();

  const lists = useMemo(() => {
    const members = { $in: [client.userId ?? ""] };
    return {
      archived: new ChannelPaginator({
        client,
        filters: { archived: true, members, type: "messaging" },
        id: "archived",
        sort,
      }),
      inbox: new ChannelPaginator({
        client,
        filters: { archived: false, members, type: "messaging" },
        id: "inbox",
        sort,
      }),
    };
  }, [client]);

  useEffect(() => {
    const paginators = [lists.inbox, lists.archived];
    paginators.forEach((paginator) =>
      client.channelManager.insertPaginator({ paginator }),
    );
    // Keeps the lists following events while no ChannelList is mounted.
    const releaseSubscriptions = client.channelManager.registerSubscriptions();

    return () => {
      releaseSubscriptions();
      paginators.forEach((paginator) => {
        client.channelManager.removePaginator(paginator);
        paginator.dispose();
      });
    };
  }, [client, lists]);

  return (
    <ChannelListsContext.Provider value={lists}>
      {children}
    </ChannelListsContext.Provider>
  );
};

export const useChannelLists = () => {
  const lists = useContext(ChannelListsContext);
  if (!lists)
    throw new Error("useChannelLists is used outside ChannelListsProvider");
  return lists;
};
```

The provider goes inside `Chat`, around the navigator. It creates the paginators again when the client changes, for example after the user logs in as someone else, and disposes the old ones.

Each list has its own `id`. The channel manager keeps every registered list current, and a channel can be in several lists at once: it's the same `Channel` object in each. How lists are queried and kept current is on [Channel list paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/channel-list-paginators/).

## Tabs and multiple channel lists

A tab navigator usually sits inside a stack. The tabs hold the lists, and the channel and thread screens are pushed on the stack above the tabs, so they cover the tab bar. With bottom tabs from `@react-navigation/bottom-tabs`, this app has an inbox, the archived channels and the thread list, using the `ChannelListsProvider` above:

```tsx title="Navigation.tsx"
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { useHeaderHeight } from "@react-navigation/elements";
import { useIsFocused, useNavigation } from "@react-navigation/native";
import {
  createNativeStackNavigator,
  type NativeStackNavigationProp,
  type NativeStackScreenProps,
} from "@react-navigation/native-stack";
import type { ChannelPaginator } from "stream-chat";
import {
  Channel,
  ChannelList,
  MessageComposer,
  MessageList,
  Thread,
  ThreadList,
  useChatContext,
} from "stream-chat-react-native";

import { useChannelLists } from "./context/ChannelListsContext";

type StackParamList = {
  Tabs: undefined;
  Channel: { cid: string };
  Thread: { threadId: string };
};

const Stack = createNativeStackNavigator<StackParamList>();
const Tab = createBottomTabNavigator();

const useStackNavigation = () =>
  useNavigation<NativeStackNavigationProp<StackParamList>>();

const ChannelListTab = ({ paginator }: { paginator: ChannelPaginator }) => {
  const navigation = useStackNavigation();

  return (
    <ChannelList
      onSelect={(channel) =>
        navigation.navigate("Channel", { cid: channel.cid })
      }
      paginator={paginator}
    />
  );
};

const InboxTab = () => <ChannelListTab paginator={useChannelLists().inbox} />;

const ArchivedTab = () => (
  <ChannelListTab paginator={useChannelLists().archived} />
);

const ThreadsTab = () => {
  const navigation = useStackNavigation();
  // A visited tab stays mounted, so the thread list is told when it's on screen.
  const isFocused = useIsFocused();

  return (
    <ThreadList
      isFocused={isFocused}
      onThreadSelect={(thread) =>
        navigation.navigate("Thread", { threadId: thread.id })
      }
    />
  );
};

const Tabs = () => (
  <Tab.Navigator>
    <Tab.Screen component={InboxTab} name="Inbox" />
    <Tab.Screen component={ArchivedTab} name="Archived" />
    <Tab.Screen component={ThreadsTab} name="Threads" />
  </Tab.Navigator>
);

const ChannelScreen = ({
  navigation,
  route,
}: NativeStackScreenProps<StackParamList, "Channel">) => {
  const { client } = useChatContext();
  const headerHeight = useHeaderHeight();
  const channel = client.channelManager.get(route.params.cid);

  if (!channel) return null;

  return (
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      topInset={headerHeight}
    >
      <MessageList
        onThreadSelect={(thread) =>
          navigation.navigate("Thread", { threadId: thread.id })
        }
      />
      <MessageComposer />
    </Channel>
  );
};

const ThreadScreen = ({
  route,
}: NativeStackScreenProps<StackParamList, "Thread">) => {
  const { client } = useChatContext();
  const headerHeight = useHeaderHeight();
  const thread = client.threads.get(route.params.threadId);

  if (!thread) return null;

  return (
    <Thread
      keyboardVerticalOffset={headerHeight}
      thread={thread}
      topInset={headerHeight}
    >
      <MessageList />
      <MessageComposer />
    </Thread>
  );
};

export const Navigation = () => (
  <Stack.Navigator>
    <Stack.Screen
      component={Tabs}
      name="Tabs"
      options={{ headerShown: false }}
    />
    <Stack.Screen component={ChannelScreen} name="Channel" />
    <Stack.Screen component={ThreadScreen} name="Thread" />
  </Stack.Navigator>
);
```

`Navigation` renders inside `NavigationContainer`, `Chat` and `ChannelListsProvider`. A few things to keep in mind with tabs:

- **A tab stays mounted once visited.** Each list keeps its channels while the user switches tabs. `ThreadList` takes `isFocused`: when its tab comes into focus, it loads its first page, or reloads to show threads that changed while it was hidden.
- **Each list has its own paginator.** Give each one an `id`, and register all of them with the channel manager. A channel that stops matching a list's filters, such as a channel the user archives, moves to the lists it matches.
- **One `OverlayProvider` covers every tab.** Keep it above the navigator. An `OverlayProvider` for each tab breaks the message menu, because the one mounted last takes the overlay over, even while its tab is hidden.
- **A channel screen inside a tab** sits above the tab bar. Its keyboard offset is the same, the header height, and the composer still pads itself for the bottom safe area. See [Keyboard and insets](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/keyboard-and-insets/).

With Expo Router, the same layout is a `(tabs)` group with a `Tabs` layout inside the root `Stack`, and the channel and thread routes next to the group.

## Used in

- [Quick start](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/quick-start/#add-a-channel-list-with-navigation)
- [Channel lists](https://getstream.io/chat/docs/sdk/react-native/v10/features/channels/channel-lists/)
- [Replies and threads](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/replies-and-threads/)
- [Push notifications](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/push-notifications/)

---

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