# Navigation

Some Stream Chat UI features require specific component placement to render correctly.

`AttachmentPicker` and `ImageGallery` must render above other components and are controlled by `OverlayProvider`. With navigation, a few steps are required to keep overlays working.

## Best Practices

- Keep `OverlayProvider` above navigation containers so overlays render on top.
- Mount `Chat` high in the tree to avoid reconnect churn.
- Pass `keyboardVerticalOffset` from navigation headers to keep input aligned.
- Manage `thread` state explicitly when navigating between channel and thread screens.
- Set `topInset` and `bottomInset` from safe area and tab bar heights.

This guide assumes [React Navigation](https://reactnavigation.org/) with [`createStackNavigator`](https://reactnavigation.org/docs/stack-navigator/).

<admonition type="note">

**If you are using another navigation solution, or utilizing [`createNativeStackNavigator`](https://reactnavigation.org/docs/native-stack-navigator/), other considerations will need to be taken depending on your navigation arrangement.**

The `createNativeStackNavigator` uses the native APIs `UINavigationController` on iOS and `Fragment` on Android. The `OverlayProvider` needs to exist in a view that can render content in front of the chat screen. Therefore, using a `fullScreenModal` with `createNativeStackNavigator`, which uses `UIModalPresentationFullScreen` on iOS and `modal` on Android, to render your chat screen will leave the `OverlayProvider` rendered behind the chat. If you are having issues, we suggest you get in touch with support, and we can find a solution to your specific navigation arrangement.

</admonition>

## Navigation Container

[`NavigationContainer`](https://reactnavigation.org/docs/navigation-container/) manages navigation state. Wrap it with `OverlayProvider` so overlays can render above screens, headers, and tab bars.

Ideally, wrap the app in `Chat` so theming, connection handling, and translations are available everywhere.

<admonition type="note">

Keeping `Chat` high in the stack avoids unmounting while connected. If it unmounts, you may need to handle reconnects yourself. The WebSocket closes ~15s after backgrounding; not handling [`appState`](https://reactnative.dev/docs/appstate) changes also affects **[push notifications](/chat/docs/sdk/react-native/v8/guides/push-notifications/)**.

</admonition>

```tsx {12,16}
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { Chat, OverlayProvider } from 'stream-chat-react-native';

const client = StreamChat.getInstance('api_key');
const Stack = createStackNavigator<{ home: undefined }>();

export const App = () =>
  <NavigationContainer>
    <OverlayProvider>
      <Chat client={client}>
        <Stack.Navigator>
          <Stack.Screen component={() => {/** App components */})} name='home' />
        </Stack.Navigator>
      </Chat>
    </OverlayProvider>
  </NavigationContainer>;
```

## Keyboard

`Channel` uses `KeyboardCompatibleView` and needs `keyboardVerticalOffset` (typically the navigation header height). Use `useHeaderHeight` from `@react-navigation/stack` and pass it to `Channel`.

```tsx
const headerHeight = useHeaderHeight();

const App = () => {
  return (
    <Channel keyboardVerticalOffset={headerHeight}>
      {/* other components inside */}
    </Channel>
  );
};
```

## Attachment Picker

`AttachmentPicker` is a keyboard-like view that attaches photos and files. Its bottom sheet comes from `OverlayProvider` and uses `topInset` and `bottomInset` for placement.

### Top Inset

`topInset` controls how high the bottom sheet can expand. It defaults to 0 (full screen). Commonly, set it to the header height for a clean fit.

```tsx
const headerHeight = useHeaderHeight();
const { setTopInset } = useAttachmentPickerContext();

useEffect(() => {
  setTopInset(headerHeight);
}, [headerHeight]);
```

<admonition type="note">

The `topInset` can be set via props on the `OverlayProvider`, or set via the `setTopInset` function provided by the `useAttachmentPickerContext` hook.

</admonition>

### Bottom Inset

`bottomInset` aligns the picker with the bottom sheet. Use the bottom safe area inset without a tab bar, or `useBottomTabBarHeight` when you have one.

```tsx {11,15,21}
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Chat, OverlayProvider } from 'stream-chat-react-native';

const client = StreamChat.getInstance('api_key');
const Stack = createStackNavigator<{ home: undefined }>();

export const Nav = () => {
  const { bottom } = useSafeAreaInsets();

  return (
    <NavigationContainer>
      <OverlayProvider bottomInset={bottom}>
        <Chat client={client}>
          <Stack.Navigator>
            <Stack.Screen component={() => {/** App components */})} name='home' />
          </Stack.Navigator>
        </Chat>
      </OverlayProvider>
    </NavigationContainer>
  );
};
```

```tsx
import React from "react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Nav } from "./Nav";

export const App = () => (
  <SafeAreaProvider>
    <Nav />
  </SafeAreaProvider>
);
```

<admonition type="note">

The `bottomInset` can be set via props on the `OverlayProvider`, or set via the `setBottomInset` function provided by the `useAttachmentPickerContext` hook.

</admonition>

### Resetting Selected Images

Selected images are tied to `MessageInput`. With a single `AttachmentPicker` and multiple `MessageInput`s, ensure `thread` state is passed appropriately or you may see duplicate uploads.

In more complex scenarios where more than one `Channel` could potentially be rendered in multiple tabs a different approach would be necessary. It is suggested that you architect an approach best for your specific scenario.

The `setSelectedImages` function can be pulled off of the `useAttachmentPickerContext` for granular control of the `AttachmentPicker` images.

## Image Gallery[](/chat/docs/sdk/react-native/v8/basics/stream_chat_with_navigation/#image-gallery)

The `ImageGallery` is populated by the `MessageList` component. `MessageList` utilizes information provided by both the `ThreadContext` and `threadList` prop to determine if the `ImageGallery` should be updated. If there is both a `thread` provided by the `ThreadContext` and the `threadList` prop is `true` on `MessageList`, or both values are falsy, the `ImageGallery` is updated appropriately.

In practice this means that if you implement a screen for the main `Channel`, and another for `Thread` that is navigated to `onThreadSelect`, you need to indicate to the main `Channel` it should not update the `ImageGallery` while the `Thread` screen is present. To do this the main `Channel` component should be given the appropriate `thread` when the `Thread` screen shown, then the `thread` removed when navigating back to the main `Channel` screen.

This can be done by keeping the current `thread` in a `context` and setting it `onThreadSelect`, then removing it `onThreadDismount`. Alternatively if a user only has a single path to and from the `Channel` screen to the `Thread` screen and back you can accomplish the same result using a local state and the [`useFocusEffect`](https://reactnavigation.org/docs/use-focus-effect/) hook from React Navigation.

<tabs>

<tabs-item value="context" label="Context">

```tsx
export const ThreadScreen = () => {
  const { channel } = useAppChannel();
  const { setThread, thread } = useAppThread();

  return (
    <Channel channel={channel} thread={thread} threadList>
      <Thread onThreadDismount={() => setThread(undefined)} />
    </Channel>
  );
};
```

```tsx
export const ChannelScreen = () => {
  const { channel } = useAppChannel();
  const { setThread, thread } = useAppThread();

  return (
    <Channel channel={channel} thread={thread}>
      <MessageList
        onThreadSelect={(selectedThread) => {
          setThread(selectedThread);
          navigation.navigate("ThreadScreen");
        }}
      />
      <MessageInput />
    </Channel>
  );
};
```

</tabs-item>

<tabs-item value="useFocusEffect" label="useFocusEffect">

```tsx
export const ChannelScreen = () => {
  const { channel } = useAppChannel();
  const [selectedThread, setSelectedThread] = useState<LocalMessage>();

  useFocusEffect(() => {
    setSelectedThread(undefined);
  });

  return (
    <Channel channel={channel} thread={selectedThread}>
      <MessageList
        onThreadSelect={(thread) => {
          setSelectedThread(thread);
          navigation.navigate("ThreadScreen", { thread });
        }}
      />
      <MessageInput />
    </Channel>
  );
};
```

</tabs-item>

</tabs>


---

This page was last updated at 2026-04-17T17:33:45.607Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v8/basics/stream_chat_with_navigation/](https://getstream.io/chat/docs/sdk/react-native/v8/basics/stream_chat_with_navigation/).