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 with createStackNavigator.

If you are using another navigation solution, or utilizing createNativeStackNavigator, 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.

NavigationContainer 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.

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 changes also affects push notifications.

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.

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.

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

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

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

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.

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>
  );
};
import React from "react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Nav } from "./Nav";

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

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

Resetting Selected Images

Selected images are tied to MessageInput. With a single AttachmentPicker and multiple MessageInputs, 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.

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 hook from React Navigation.

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

  return (
    <Channel channel={channel} thread={thread} threadList>
      <Thread onThreadDismount={() => setThread(undefined)} />
    </Channel>
  );
};
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>
  );
};