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 = () =>
<OverlayProvider>
<NavigationContainer>
<Chat client={client}>
<Stack.Navigator>
<Stack.Screen component={() => {/** App components */})} name='home' />
</Stack.Navigator>
</Chat>
</NavigationContainer>
</OverlayProvider>;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
- Place
OverlayProvideraboveNavigationContainerso overlays render on top of your navigation UI. - Mount
Chathigh in the tree to avoid reconnect churn. - Set
keyboardVerticalOffsetto your navigation header height and keep it the same across platforms. - Manage
threadstate explicitly when navigating between channel and thread screens. - Set
topInsetandbottomInsetonChannel, and use the attachment picker context setters only when those values must change dynamically.
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.
Navigation Container
NavigationContainer manages navigation state. Put OverlayProvider above it 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.
The examples below also use react-native-safe-area-context. Install it if you are following this guide with React Navigation.
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.
Keyboard
Channel uses KeyboardCompatibleView and accepts keyboardVerticalOffset. Pass the navigation header height on both Android and iOS so the composer moves consistently with the screen's top chrome.
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. Configure its screen-level topInset and bottomInset on Channel so the picker stays aligned with the same chat layout that owns the composer.
Top Inset
topInset controls how high the bottom sheet can expand. It defaults to 0 (full screen). For most screens, pass the navigation header height to Channel.
const headerHeight = useHeaderHeight();
<Channel topInset={headerHeight}>{/* chat UI */}</Channel>;Use setTopInset from useAttachmentPickerContext only when the value must change after the Channel has mounted.
Bottom Inset
bottomInset accounts for space below MessageComposer. Set it on Channel using the bottom safe area inset when there is no tab bar, or useBottomTabBarHeight when the tab bar owns that space.
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
Channel,
MessageComposer,
MessageList,
} from "stream-chat-react-native";
export const ChannelScreen = () => {
const { bottom } = useSafeAreaInsets();
return (
<Channel bottomInset={bottom}>
<MessageList />
<MessageComposer />
</Channel>
);
};Use setBottomInset from useAttachmentPickerContext when the inset must be updated dynamically, and use MessageComposer's bottomInset prop only as a per-composer override.
Resetting Selected Images
Selected images are tied to MessageComposer. With a single AttachmentPicker and multiple MessageComposers, 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
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");
}}
/>
<MessageComposer />
</Channel>
);
};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 });
}}
/>
<MessageComposer />
</Channel>
);
};