Chat Integration
Stream's Chat and Video SDKs integrate seamlessly for calling apps with chat or chat apps with calling features.
Installation
This guide uses stream-chat-react-native v10 and stream-chat v10. Install them following the chat SDK's installation guide:
yarn add stream-chat-react-native@10 stream-chat@10The chat SDK needs React Native 0.79 or later. If your chat code is on an earlier version, follow the upgrade guide first.
Creating and Passing Chat Client
The chat SDK's Quick start explains how the chat client is created and the user connected.
Wrapping the chat client
Create a ChatWrapper component at the app root to provide the client to all children:
import React, { PropsWithChildren } from "react";
import { ActivityIndicator, StyleSheet, View } from "react-native";
import {
Chat,
OverlayProvider,
Streami18n,
useCreateChatClient,
} from "stream-chat-react-native";
const STREAM_API_KEY = "your-api-key";
const user = { id: "your-user-id", name: "your-user-name" };
const token = "your-user-token";
const streami18n = new Streami18n({ language: "en" });
export const ChatWrapper = ({ children }: PropsWithChildren) => {
const chatClient = useCreateChatClient({
apiKey: STREAM_API_KEY,
tokenOrProvider: token,
userData: user,
});
if (!chatClient) {
// Show a loader until the Chat client connects
return (
<View style={styles.container}>
<ActivityIndicator size="large" />
</View>
);
}
return (
<OverlayProvider i18nInstance={streami18n}>
<Chat client={chatClient} i18nInstance={streami18n}>
{children}
</Chat>
</OverlayProvider>
);
};
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", alignItems: "center" },
});userData is the user to connect: only id is required, and a profile picture goes in image.
It is essential for you to wrap the GestureHandlerRootView, OverlayProvider and Chat component on the root of your chat integration hierarchy.
GestureHandlerRootView should be wrapped into the root of your application as follows:
import React from "react";
import { StyleSheet } from "react-native";
import { StreamCall, StreamVideo } from "@stream-io/video-react-native-sdk";
import { VideoCallUI } from "src/components/VideoCallUI";
import { GestureHandlerRootView } from "react-native-gesture-handler";
export default function App() {
// ...
return (
<GestureHandlerRootView style={styles.container}>
<StreamVideo client={client}>
<StreamCall call={call}>
<VideoCallUI />
</StreamCall>
</StreamVideo>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});Adding Chat into Video
Step 1: Wrapping the ChatWrapper
Wrap ChatWrapper with StreamVideo from the Video Calling Tutorial:
import React from "react";
import { StyleSheet } from "react-native";
import { StreamCall, StreamVideo } from "@stream-io/video-react-native-sdk";
import { VideoCallUI } from "./components/VideoCallUI";
import { ChatWrapper } from "./src/components/ChatWrapper";
import { GestureHandlerRootView } from "react-native-gesture-handler";
export default function App() {
// ...
return (
<ChatWrapper>
<GestureHandlerRootView style={styles.container}>
<StreamVideo client={client}>
<StreamCall call={call}>
<VideoCallUI />
</StreamCall>
</StreamVideo>
</GestureHandlerRootView>
</ChatWrapper>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});Step 2: Creating the Chat window/screen
Use Channel, MessageList, and MessageComposer components. Use call ID as channel ID for distinction. Channel starts watching the channel when it mounts.

import React, { useMemo } from "react";
import { StyleSheet, View } from "react-native";
import {
Channel,
MessageComposer,
MessageList,
useChatContext,
} from "stream-chat-react-native";
const CHANNEL_TYPE = "messaging";
const CHANNEL_ID = "your-call-id";
export const ChatScreen = () => {
const { client } = useChatContext();
const channel = useMemo(
() => client.channel(CHANNEL_TYPE, CHANNEL_ID),
[client],
);
return (
<View style={styles.wrapper}>
<Channel channel={channel}>
<MessageList />
<MessageComposer />
</Channel>
</View>
);
};
const styles = StyleSheet.create({
wrapper: { flex: 1 },
});When the chat screen sits below a navigation header, pass the header's height to Channel as keyboardVerticalOffset and topInset, so the composer and the attachment picker stay clear of it. Keyboard and insets explains both.
Step 3: Open the chat window
Create custom CallControls with ChatButton. The onPressHandler opens ChatScreen.

import React, { useCallback } from "react";
import { SafeAreaView, StyleSheet, View } from "react-native";
import {
CallContent,
CallControlsButton,
HangUpCallButton,
ToggleAudioPublishingButton,
ToggleCameraFaceButton,
ToggleVideoPublishingButton,
} from "@stream-io/video-react-native-sdk";
type ChatButtonProps = {
onPressHandler?: () => void;
};
export const ChatButton = ({ onPressHandler }: ChatButtonProps) => {
return (
<CallControlsButton onPress={onPressHandler}>
{/* Your Chat Icon here */}
</CallControlsButton>
);
};
export type CallControlsComponentProps = {
onChatOpenHandler?: () => void;
onHangupCallHandler?: (err?: Error) => void;
};
export const CallControlsComponent = ({
onChatOpenHandler,
onHangupCallHandler,
}: CallControlsComponentProps) => {
return (
<View style={styles.callControlsWrapper}>
<ChatButton onPressHandler={onChatOpenHandler} />
<ToggleVideoPublishingButton />
<ToggleAudioPublishingButton />
<ToggleCameraFaceButton />
<HangUpCallButton onHangupCallHandler={onHangupCallHandler} />
</View>
);
};
export const VideoCallUI = () => {
const onChatOpenHandler = useCallback(() => {
// handle how to open the Chat window/screen.
// You can use this to navigate to the chat screen, open a bottom sheet modal, etc.
}, []);
const CustomControlsComponent = useCallback(() => {
return <CallControlsComponent onChatOpenHandler={onChatOpenHandler} />;
}, [onChatOpenHandler]);
return (
<SafeAreaView style={styles.container}>
<CallContent CallControls={CustomControlsComponent} />
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#272A30" },
callControlsWrapper: {
flexDirection: "row",
justifyContent: "space-evenly",
paddingVertical: 12,
zIndex: 2,
backgroundColor: "#272A30",
},
});Step 4: Showing unread count indicator for new chats
Watch the channel and read the current user's unread count from the channel's state. The chat client keeps the count up to date while the channel is watched, and resets it when the user reads the messages.

import { useEffect, useMemo } from "react";
import type { ChannelStateData } from "stream-chat";
import { useChatContext, useStateStore } from "stream-chat-react-native";
import { useCall } from "@stream-io/video-react-native-sdk";
const CHANNEL_TYPE = "messaging";
const readSelector = (state: ChannelStateData) => ({ read: state.read });
/**
* Returns the current user's unread message count in the call's channel.
*/
export const useUnreadCount = () => {
const { client } = useChatContext();
const call = useCall();
const callId = call?.id;
const channel = useMemo(
() => (callId ? client.channel(CHANNEL_TYPE, callId) : undefined),
[client, callId],
);
// Watch the channel, so the client keeps its state up to date
useEffect(() => {
if (!channel) return;
channel.watch().catch((error) => {
console.warn("Failed to watch the channel", error);
});
}, [channel]);
const { read } = useStateStore(channel?.state, readSelector) ?? {};
if (!client.userId || !read) return 0;
return read[client.userId]?.unread_messages ?? 0;
};client.channel() returns the same Channel instance to every component that asks for that channel, including the chat screen. The hook therefore doesn't call Channel.stopWatching() when it un-mounts: that would stop watching the channel for the chat screen too. Channel.watch() rejects when the client isn't connected, so handle its error.
Pass unread count via unreadBadgeCountIndicator in CallControls:
import React, { useCallback } from "react";
import { SafeAreaView, StyleSheet, Text, View } from "react-native";
import {
CallContent,
CallControlsButton,
HangUpCallButton,
} from "@stream-io/video-react-native-sdk";
import { useUnreadCount } from "../hooks/useUnreadCount";
export const BadgeCountIndicator = ({
count,
}: {
count: number | undefined;
}) => {
// Don't show badge if count is 0 or undefined
if (!count) return null;
return (
<View style={styles.badge}>
<Text style={styles.badgeText}>{count}</Text>
</View>
);
};
type ChatButtonProps = {
onPressHandler?: () => void;
unreadBadgeCount?: number;
};
export const ChatButton = ({
onPressHandler,
unreadBadgeCount,
}: ChatButtonProps) => {
return (
<CallControlsButton onPress={onPressHandler}>
{/* Your Chat Icon here */}
<BadgeCountIndicator count={unreadBadgeCount} />
</CallControlsButton>
);
};
export type CallControlsComponentProps = {
onChatOpenHandler?: () => void;
onHangupCallHandler?: (err?: Error) => void;
unreadBadgeCount?: number;
};
export const CallControlsComponent = ({
onChatOpenHandler,
onHangupCallHandler,
unreadBadgeCount,
}: CallControlsComponentProps) => {
return (
<View style={styles.callControlsWrapper}>
{/* ... */}
<ChatButton
onPressHandler={onChatOpenHandler}
unreadBadgeCount={unreadBadgeCount}
/>
<HangUpCallButton onHangupCallHandler={onHangupCallHandler} />
</View>
);
};
export const VideoCallUI = () => {
const unreadBadgeCount = useUnreadCount();
const onChatOpenHandler = useCallback(() => {
// handle how to open the Chat window/screen.
// You can use this to navigate to the chat screen, open a modal, etc.
}, []);
const CustomControlsComponent = useCallback(() => {
return (
<CallControlsComponent
onChatOpenHandler={onChatOpenHandler}
unreadBadgeCount={unreadBadgeCount}
/>
);
}, [onChatOpenHandler, unreadBadgeCount]);
return (
<SafeAreaView style={styles.container}>
<CallContent CallControls={CustomControlsComponent} />
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1 },
callControlsWrapper: {
flexDirection: "row",
justifyContent: "space-evenly",
paddingVertical: 12,
},
badge: {
borderRadius: 10,
paddingHorizontal: 5,
paddingVertical: 2,
backgroundColor: "#20E070",
},
badgeText: { fontSize: 12, fontWeight: "bold", color: "#000000" },
});Adding Video into Chat
Add video calling to chat for messenger-like functionality.





Use ChatWrapper from earlier setup.
Step 1: Setting up navigation
Use React Navigation for navigation.
Installing the dependencies
Install native stack navigator packages:
yarn add @react-navigation/native @react-navigation/elements @react-navigation/native-stack react-native-screens react-native-safe-area-contextInstall pods:
npx pod-installYou can also follow the Getting started guide to install the React Navigation dependencies effectively.
Wrapping the App in NavigationContainer
Wrap with NavigationContainer for state management:
import React from "react";
import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";
export default function App() {
// ...
return (
<GestureHandlerRootView style={styles.container}>
<NavigationContainer>{/* Rest of the code */}</NavigationContainer>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});Step 2: Creating the screens
Create three screens:
- Channel list screen - Lists all channels
- Channel screen - Messages, composer, and call button
- Thread screen - Replies to a message
The channel screen gets the channel's cid as a route param, and the thread screen gets the thread's id. Set up screen types:
export type NavigationStackParamsList = {
ChannelListScreen: undefined;
ChannelScreen: { cid: string };
ThreadScreen: { threadId: string };
};Channel List Screen:

The ChannelList component renders a ChannelPaginator: it holds the filters and sort order of the list. Register it on the client's channel manager, which keeps the list up to date:
import React, { useEffect, useMemo } from "react";
import { StyleSheet, View } from "react-native";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import { ChannelList, useChatContext } from "stream-chat-react-native";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import type { NavigationStackParamsList } from "src/types";
const sort: SortParamRequest[] = [
{ field: "last_message_at", direction: -1 },
// Your sort options
];
type ChannelListScreenProps = NativeStackScreenProps<
NavigationStackParamsList,
"ChannelListScreen"
>;
export function ChannelListScreen({ navigation }: ChannelListScreenProps) {
const { client } = useChatContext();
const paginator = useMemo(() => {
const filters: ChannelFilters = {
type: "messaging",
members: { $in: [client.userId ?? ""] },
// Your filters
};
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 (
<View style={styles.container}>
<ChannelList
onSelect={(channel) => {
navigation.navigate("ChannelScreen", { cid: channel.cid });
}}
paginator={paginator}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});Channel Screen:

The channel list's paginator keeps the channel in client.channelManager, so the channel screen looks it up by its cid. Opening a message's replies navigates to the thread screen with the thread's id:
import React, { useEffect } from "react";
import { useHeaderHeight } from "@react-navigation/elements";
import {
Channel,
MessageComposer,
MessageList,
useChatContext,
useOverlayContext,
} from "stream-chat-react-native";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import type { NavigationStackParamsList } from "src/types";
import { Platform, StyleSheet, View } from "react-native";
type ChannelScreenProps = NativeStackScreenProps<
NavigationStackParamsList,
"ChannelScreen"
>;
export function ChannelScreen({ navigation, route }: ChannelScreenProps) {
const { client } = useChatContext();
const channel = client.channelManager.get(route.params.cid);
const headerHeight = useHeaderHeight();
const { overlay } = useOverlayContext();
useEffect(() => {
navigation.setOptions({
gestureEnabled: Platform.OS === "ios" && overlay === "none",
});
}, [navigation, overlay]);
if (!channel) return null;
return (
<Channel
channel={channel}
keyboardVerticalOffset={headerHeight}
topInset={headerHeight}
>
<View style={styles.container}>
<MessageList
onThreadSelect={(thread) => {
navigation.navigate("ThreadScreen", { threadId: thread.id });
}}
/>
<MessageComposer />
</View>
</Channel>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});Uses Channel, MessageList, MessageComposer.
Thread Screen:
The client keeps a thread it opened, so the thread screen looks it up by its id. Thread takes the same keyboardVerticalOffset and topInset as Channel:
import React from "react";
import { useHeaderHeight } from "@react-navigation/elements";
import {
MessageComposer,
MessageList,
Thread,
useChatContext,
} from "stream-chat-react-native";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import type { NavigationStackParamsList } from "src/types";
type ThreadScreenProps = NativeStackScreenProps<
NavigationStackParamsList,
"ThreadScreen"
>;
export function ThreadScreen({ route }: ThreadScreenProps) {
const { client } = useChatContext();
const thread = client.threads.get(route.params.threadId);
const headerHeight = useHeaderHeight();
if (!thread) return null;
return (
<Thread
keyboardVerticalOffset={headerHeight}
thread={thread}
topInset={headerHeight}
>
<MessageList />
<MessageComposer />
</Thread>
);
}Set up Native Stack Navigator:
import React from "react";
import { StyleSheet } from "react-native";
import { NavigationStackParamsList } from "./src/types";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";
import { ChannelListScreen } from "./src/screens/ChannelListScreen";
import { ChannelScreen } from "./src/screens/ChannelScreen";
import { ThreadScreen } from "./src/screens/ThreadScreen";
const Stack = createNativeStackNavigator<NavigationStackParamsList>();
const Messenger = () => {
return (
<Stack.Navigator>
<Stack.Screen name="ChannelListScreen" component={ChannelListScreen} />
<Stack.Screen name="ChannelScreen" component={ChannelScreen} />
<Stack.Screen name="ThreadScreen" component={ThreadScreen} />
</Stack.Navigator>
);
};
export default function App() {
// ...
return (
<GestureHandlerRootView style={styles.container}>
<NavigationContainer>
<Messenger />
</NavigationContainer>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});Step 3: Wrapping the ChatWrapper
Wrap ChatWrapper to provide chat client:
import React from "react";
import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";
import { ChatWrapper } from "./src/components/ChatWrapper";
// ...
export default function App() {
// ...
return (
<GestureHandlerRootView style={styles.container}>
<NavigationContainer>
<ChatWrapper>
<Messenger />
</ChatWrapper>
</NavigationContainer>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});Step 4: Creating and passing the Video Wrapper
Create a video wrapper per the Video Call Tutorial:
import React, { PropsWithChildren, useEffect, useState } from "react";
import {
StreamVideo,
StreamVideoClient,
} from "@stream-io/video-react-native-sdk";
const STREAM_API_KEY = "your-api-key";
const user = { id: "your-user-id", name: "your-user-name" };
const token = "your-user-token";
export const VideoWrapper = ({ children }: PropsWithChildren) => {
const [videoClient, setVideoClient] = useState<StreamVideoClient>();
useEffect(() => {
const _videoClient = new StreamVideoClient({
apiKey: STREAM_API_KEY,
user,
token,
});
setVideoClient(_videoClient);
return () => {
_videoClient.disconnectUser().catch((error) => {
console.error("Failed to disconnect", error);
});
setVideoClient(undefined);
};
}, []);
if (!videoClient) return null;
return (
<StreamVideo client={videoClient} language={"en"}>
{children}
</StreamVideo>
);
};Wrap VideoWrapper:
import React from "react";
import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";
import { ChatWrapper } from "./src/components/ChatWrapper";
import { VideoWrapper } from "./src/components/VideoWrapper";
// ...
export default function App() {
// ...
return (
<GestureHandlerRootView style={styles.container}>
<NavigationContainer>
<ChatWrapper>
<VideoWrapper>
<Messenger />
</VideoWrapper>
</ChatWrapper>
</NavigationContainer>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});Step 5: Passing the call to StreamCall
Use useCalls for pending calls and StreamCall to provide call context:
import React, { useCallback, useEffect } from "react";
import { StreamCall, useCalls } from "@stream-io/video-react-native-sdk";
import { Alert } from "react-native";
export const Calls = () => {
const calls = useCalls();
const handleMoreCalls = useCallback(() => {
const lastCallCreatedBy = calls[1].state.createdBy;
Alert.alert(
`Incoming call from ${
lastCallCreatedBy?.name ?? lastCallCreatedBy?.id
}, only 1 call at a time is supported`,
);
}, [calls]);
// Reset the state of the show variable when there are no calls.
useEffect(() => {
if (calls.length > 1) {
handleMoreCalls();
}
}, [calls.length, handleMoreCalls]);
const firstCall = calls[0];
if (!firstCall) return null;
return <StreamCall call={firstCall}>{/* Rest of the code */}</StreamCall>;
};We currently support one call at a time, and therefore we show an alert, if a user gets multiple calls while they are in the call already. You can handle it in a different way.
This is the reason why we pass calls[0] to the StreamCall component, for now.
Step 6: Using the RingingCallContent
Use RingingCallContent to handle Incoming, Outgoing, CallPreparing, and CallContent states:
import React from "react";
import { StyleSheet, View } from "react-native";
import {
RingingCallContent,
StreamCall,
useCalls,
} from "@stream-io/video-react-native-sdk";
export const Calls = () => {
const calls = useCalls();
// ...
const firstCall = calls[0];
if (!firstCall) return null;
return (
<StreamCall call={firstCall}>
<View style={StyleSheet.absoluteFill}>
<RingingCallContent />
</View>
</StreamCall>
);
};Render Calls after the navigator, inside VideoWrapper. It fills the screen, so a call shows above the chat screens:
import React from "react";
import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";
import { ChatWrapper } from "./src/components/ChatWrapper";
import { VideoWrapper } from "./src/components/VideoWrapper";
import { Calls } from "./src/components/Calls";
// ...
export default function App() {
// ...
return (
<GestureHandlerRootView style={styles.container}>
<NavigationContainer>
<ChatWrapper>
<VideoWrapper>
<Messenger />
<Calls />
</VideoWrapper>
</ChatWrapper>
</NavigationContainer>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});Step 7: Adding a call button to the Channel Header
Add a call button to the channel screen's header with the headerRight option of Stack.Screen. It gets the channel's cid from the route params:

<Stack.Screen
name="ChannelScreen"
component={ChannelScreen}
options={({ route }) => ({
headerRight: () => <CreateCallButton cid={route.params.cid} />,
})}
/>CreateCallButton implementation. It rings every member of the channel:
import React, { useCallback } from "react";
import { Pressable, StyleSheet } from "react-native";
import { useChatContext } from "stream-chat-react-native";
import {
type MemberRequest,
useStreamVideoClient,
} from "@stream-io/video-react-native-sdk";
export const CreateCallButton = ({ cid }: { cid: string }) => {
const { client } = useChatContext();
const videoClient = useStreamVideoClient();
const createCallHandler = useCallback(async () => {
const channel = client.channelManager.get(cid);
if (!videoClient || !channel) return;
const members = Object.values(channel.state.members).map<MemberRequest>(
(member) => ({ user_id: member.user_id! }),
);
try {
// use a unique call ID, such as a UUID
const call = videoClient.call("default", "random-call-id");
await call.getOrCreate({
ring: true,
data: {
// more timeout to cancel the call automatically so that it works when callee's app is in quit state
settings_override: {
ring: {
auto_cancel_timeout_ms: 30000,
incoming_call_timeout_ms: 5000,
},
},
custom: { channelCid: cid },
members,
},
});
} catch (error) {
console.log("Failed to createCall", error);
}
}, [client, cid, videoClient]);
return (
<Pressable onPress={createCallHandler} style={styles.icon}>
{/* Call icon component */}
</Pressable>
);
};
const styles = StyleSheet.create({
icon: { height: 18, width: 18 },
});Pressing the call button sends an outgoing call. Recipients can accept/reject via the Incoming call screen.
Further Steps
Use this template for chat-primary apps with video features.
Related guides: