This tutorial teaches you how to build a fully featured mobile messaging app with the React Native CLI and Stream's Chat React Native SDK - channel list, message composer, reactions, threads, typing indicators, and file uploads, running on both iOS and Android. You can use it as the foundation for any type of in-app chat.
This page works for both humans and AI coding agents: every step is a file operation or a shell command, every step has a verification checkpoint, and the few steps that need a human are explicitly marked.
This chat tutorial uses Stream's edge network for optimal performance. Stream powers chat, activity feeds and video for thousands of apps and over a billion end users.
This tutorial uses Stream Chat React Native v10. It has been tested with the following versions:
- React Native 0.88.1. The SDK needs React Native 0.79 or later, with the New Architecture enabled
- Stream Chat React Native SDK 10.0.0 with
stream-chat10.0.0 react-native-reanimated4.7.1,react-native-worklets0.13.1,react-native-gesture-handler3.4.0,react-native-safe-area-context5.11.1,react-native-svg15.16.0,react-native-teleport1.2.3 and@react-native-community/netinfo12.0.1- React Navigation:
@react-navigation/native7.5.1,@react-navigation/native-stack7.21.0,@react-navigation/elements2.9.45 andreact-native-screens4.30.1 - Node 22.18 or later, which
stream-chat10 needs to install, and Yarn 3.6.4 (the version the React Native template sets up)
The SDK supports the React Native New Architecture only. If your app still uses the old architecture, migrate that first before integrating the SDK. See Installation.
We also have a sample project available if you want to compare your setup against a working app.
Using Expo instead of the React Native CLI? Follow the Expo chat tutorial.
Choose your path
This tutorial has two paths. In Path A you let your AI agent build the app. In Path B you build it yourself, step by step, on the React Native CLI. Both paths end with the same working chat app, so pick one instead of working through both.
Path A - Let your AI agent build it (recommended)
Install the Stream CLI once, then add the skills. This gives Claude Code, Cursor, or Codex the React Native integration patterns and current SDK APIs, so it builds against real docs instead of stale training data.
# Install the getstream CLI
curl -fsSL https://getstream.io/cli.sh | bash
# Install the skills (router, docs, and builder). The React Native pack installs
# on demand the first time it's needed, or add it explicitly:
getstream skills
getstream skills stream-react-nativeThen ask your agent:
/stream-react-native Build a React Native CLI chat app with a channel list screen,
a channel screen, and a thread screen. Use Stream Chat React Native v10
(stream-chat-react-native 10.0.0 or later). Provision credentials with the CLI
(create/select my org and app, mint a token), or fall back to the tutorial demo
credentials if I'm not logged in.Where you come in. The agent handles credentials and code itself. It stops for you twice: when getstream init opens your browser to sign in and pick an app (new accounts get an organization created in that flow), and when the app first runs on a simulator or emulator - launching one and trusting a native build is a human call.
Want to get the sign-in out of the way first? Run getstream init before you prompt the agent - it picks up the initialized project from there.
Checkpoint: build the app on a simulator or device if the agent hasn't (yarn ios or yarn android). You should see a channel list, be able to open a channel, and send a message.
Skim Important Building Blocks if you want to understand what the agent wrote before you extend it.
Path B - Build it manually
Choose this path if you'd rather write the code yourself, or you want to understand what the SDK is doing before you hand it to an agent. Code blocks titled App.tsx show the parts of that file that changed at that point in the tutorial; comments such as // ...rest of the code mark everything you keep as is.
For AI assistants reading this page: append
.mdto any Stream docs URL for a clean Markdown version. A condensed index for the React Native Chat SDK is athttps://getstream.io/chat/docs/sdk/react-native/llms.txt. Prefer these over parsing HTML.
Important Building Blocks
Five pieces, one mental model:
-
useCreateChatClient- creates theStreamChatclient and manages its connection. The client abstracts API calls into methods, holds the state of channels, threads and message lists, and handles real-time events. Use this hook once per application; anywhere further down the tree, reach foruseChatContextinstead. -
OverlayProvider- the outermost Stream component, mounted near the root of your app. It hosts the full-screen image viewer and the message context menu. -
ChatandChannel- React context providers.Chatholds the client and connection state;Channelgives one channel to the message list and the composer inside it.Theme and translations come from both
OverlayProviderandChat- each one providesThemeContextandTranslationContext, so components read whichever is closest above them. -
UI components -
ChannelList,MessageList,MessageComposer, andThread.ChannelListrenders aChannelPaginatorthat your app creates, andThreadrenders next toChannelon a screen of its own. Use them as-is, or replace them as you see fit. -
WithComponents- the customization surface. Swap any SDK-owned view for your own; everything you don't name keeps its default. It nests, so inner overrides merge over outer ones and the closest provider wins.
The SDK does not drill props - components read what they need from context, via hooks such as useChannel, useMessageContext, and useTheme. The providers and context hooks page lists every context, the hook that reads it, and the component that provides it.
You'll get a working app with the default UI first, then theme it, then replace individual components. Deeper customizations are linked at the end.
Prerequisites
You need a working React Native development environment. Follow the Set Up Your Environment guide, choosing React Native CLI and the platform you want to build for. Use Node 22.18 or later, because stream-chat 10 needs it to install.
Step 0 - Set up the project
To get started, create a new application with the React Native CLI:
# Initialize the app
npx @react-native-community/cli@latest init MyStreamChatApp --pm yarn --install-pods false
# Navigate to the app directory
cd MyStreamChatApp--pm yarn keeps the project on Yarn, which the rest of this tutorial uses; drop it to stay on npm and substitute npm install / npm run as you go.
Install the Stream Chat React Native SDK together with stream-chat:
yarn add stream-chat-react-native stream-chatThe SDK depends on stream-chat, the JavaScript client that holds the state. Your app imports from it too, for example ChannelPaginator and the types, so add it next to the SDK and upgrade both together. Exactly one copy of stream-chat must resolve.
Stream Chat requires a set of peer dependencies for the core React Native UI experience. Follow the installation steps for each dependency to ensure everything is configured correctly.
Install the required peer dependencies:
yarn add @react-native-community/netinfo react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-teleport react-native-workletsThen finish the native install:
npx pod-installApplication level setup
The most important steps to get started are:
- Add the Babel plugin for
react-native-reanimatedto yourbabel.config.jsfile, as the last plugin:
module.exports = {
// other config
plugins: [
// other plugins
"react-native-worklets/plugin",
],
};- Import
react-native-gesture-handlerat the top of yourindex.jsfile:
import "react-native-gesture-handler";
import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";
AppRegistry.registerComponent(appName, () => App);- Wrap your app root with both
SafeAreaProviderandGestureHandlerRootView. We will do that inApp.tsxin Step 2.
Also, follow the steps mentioned in the links below for corresponding dependencies:
react-native- additional installation steps
If you enable optional features such as audio recording, camera access, or media library access, make sure you also add the required platform permissions for those features.
Now you should be able to run the app:
yarn ios # or npx react-native run-iosIn order to be able to run the app on iOS, please make sure that your
./ios/.xcode.env.localcontains aNODE_BINARYthat matches the version and path of the installednodebinary that you use.You can see the correct path by running
which nodein your terminal.
yarn android # or npx react-native run-androidCheckpoint: stream-chat-react-native, stream-chat and the peer dependencies appear in package.json, and the app builds and shows the React Native welcome screen on a simulator or emulator.


Step 1 - Define the type system
Declare the SDK interfaces in your app so Stream's extended types resolve correctly. The module declaration needs to take place so that types are properly resolved within your application. You can read more about this in Data conventions.
To do this, you can create a new TypeScript declaration file and add the following code to it:
import "stream-chat";
import type {
DefaultAttachmentData,
DefaultChannelData,
DefaultEventData,
DefaultMemberData,
DefaultMessageData,
DefaultPollData,
DefaultPollOptionData,
DefaultReactionData,
DefaultThreadData,
DefaultUserData,
} from "stream-chat-react-native";
declare module "stream-chat" {
/* eslint-disable @typescript-eslint/no-empty-object-type */
interface CustomAttachmentData extends DefaultAttachmentData {}
interface CustomChannelData extends DefaultChannelData {}
interface CustomEventData extends DefaultEventData {}
interface CustomMemberData extends DefaultMemberData {}
interface CustomUserData extends DefaultUserData {}
interface CustomMessageData extends DefaultMessageData {}
interface CustomPollOptionData extends DefaultPollOptionData {}
interface CustomPollData extends DefaultPollData {}
interface CustomReactionData extends DefaultReactionData {}
interface CustomThreadData extends DefaultThreadData {}
/* eslint-enable @typescript-eslint/no-empty-object-type */
}This will make sure that all of the SDK interfaces are properly resolved and the types are correct. Additionally, this will allow you to declare your own custom data if your integration requires it. Your custom fields are typed under custom, for example message.custom and channel.data.custom.
Checkpoint: custom-types.d.ts exists at the project root.
Step 2 - Add navigation
The Stream Chat SDK does not handle navigation, but libraries like React Navigation make it easy to set up the channel list, channel, and thread screens we need for the application.
Install the following packages to get started with React Navigation, as mentioned in their documentation. This tutorial uses its native stack.
yarn add @react-navigation/native @react-navigation/native-stack @react-navigation/elements react-native-screensThen finish the native install:
npx pod-installWe'll set up a simple Navigation stack to hold the necessary screens for navigation in our app, and start with a basic HomeScreen, which we will replace later with chat related screens.
You can copy-paste the following code into the App.tsx file.
import React from "react";
import { StatusBar, StyleSheet, Text, View } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
const Stack = createNativeStackNavigator();
const HomeScreen = () => (
<View style={styles.centered}>
<Text>Home Screen</Text>
</View>
);
const NavigationStack = () => (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen component={HomeScreen} name="Home" />
</Stack.Navigator>
</NavigationContainer>
);
export default function App() {
return (
<SafeAreaProvider>
<StatusBar barStyle="dark-content" />
<GestureHandlerRootView style={styles.container}>
<NavigationStack />
</GestureHandlerRootView>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
centered: {
alignItems: "center",
flex: 1,
justifyContent: "center",
},
});The StatusBar keeps the status bar icons dark, so they stay visible on the light header on Android.
Checkpoint: the app shows a "Home" header and the text Home Screen centered on the page.


Step 3 - Credentials
The next step needs four values: an API key that identifies your Stream application to our servers, a user ID and user token that authorize the current chat user, and a user name used as their display name.
There are two ways to get them. Pick one, then finish with the "Store the credentials" step.
Option 1 - Your own Stream app, via the Stream CLI
The getstream CLI provisions all of it in one flow. Run these from the MyStreamChatApp directory you created in Step 0.
1. Install the CLI (skip if you did this in Path A):
curl -fsSL https://getstream.io/cli.sh | bash2. Initialize the project. This one command authenticates you, lets you create or select an organization and app, and writes the project credentials. New to Stream? The sign-in flow creates your organization. Already have an org or an app? It lets you pick them.
getstream initHuman checkpoint: getstream init opens a browser to authenticate. Agents: run it, then ask the human to finish signing in before continuing. It's required first - env, token, and api commands fail with "stream project is not initialized" until it runs.
3. Write out your API key. The CLI has no bare React Native CLI target yet, so use the Expo target to get the key onto disk:
getstream env --target expoThis writes EXPO_PUBLIC_STREAM_API_KEY into .env and adds that file to .gitignore. The API secret is never written to a client target. A bare React Native CLI app has no .env loader, so copy the value into chatConfig.ts below. For a real app, read it through a library such as react-native-config instead of hardcoding it.
4. Mint a user token for a user in your app (never expiring by default; add a TTL for production-like testing):
getstream token cleo
getstream token cleo --ttl 1d5. Seed a channel so your first launch isn't an empty list. Create the users first, then the channel:
getstream api UpdateUsers --request '{"users":{"cleo":{"id":"cleo","name":"Cleo"},"cara":{"id":"cara","name":"Cara"}}}'
getstream api GetOrCreateChannel --type messaging --id general \
--request '{"data":{"created_by_id":"cleo","custom":{"name":"General"},"members":[{"user_id":"cleo"},{"user_id":"cara"}]}}'You can also mint tokens with our token (JWT) generator utility. Learn more in the Tokens & Authentication documentation.
Option 2 - Pre-filled tutorial credentials, no account
Want to skip account setup entirely? Use these values in the "Store the credentials" step:
export const chatApiKey = "REPLACE_WITH_API_KEY";
export const chatUserId = "REPLACE_WITH_USER_ID";
export const chatUserName = "REPLACE_WITH_USER_NAME";
export const chatUserToken = "REPLACE_WITH_USER_TOKEN";Swap in your own credentials from Option 1 before building anything real. Tutorial credentials are shared and short lived.
Store the credentials
Whichever option you picked, put the four values in a small config file:
export const chatApiKey = "<your API key>";
export const chatUserId = "<your user id>";
export const chatUserName = "<your user name>";
export const chatUserToken = "<your user token>";For production apps, generate user tokens on your backend and return them to the client instead of hardcoding them in the app.
Checkpoint: chatConfig.ts holds four non-placeholder values, and the API key and token belong to the same Stream app.
Step 4 - Connect the user
Before rendering any chat UI, create a StreamChat client and connect the current user. The easiest way to do that in a React Native app is with the useCreateChatClient hook.
Update the NavigationStack component so it creates the client before rendering the navigator:
import React from "react";
import { StatusBar, StyleSheet, Text, View } from "react-native";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { useCreateChatClient } from "stream-chat-react-native";
import {
chatApiKey,
chatUserId,
chatUserName,
chatUserToken,
} from "./chatConfig";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { NavigationContainer } from "@react-navigation/native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
// ...rest of the code
const user = {
id: chatUserId,
name: chatUserName,
};
const NavigationStack = () => {
const chatClient = useCreateChatClient({
apiKey: chatApiKey,
userData: user,
tokenOrProvider: chatUserToken,
});
if (!chatClient) {
return (
<SafeAreaView>
<Text>Loading chat ...</Text>
</SafeAreaView>
);
}
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen component={HomeScreen} name="Home" />
</Stack.Navigator>
</NavigationContainer>
);
};Note: Make sure you use the
useCreateChatClienthook only once per application. If you need the client instance somewhere down in the component tree, use theuseChatContexthook exported bystream-chat-react-nativeto access it.
Checkpoint: the app briefly shows Loading chat ... and then falls back to the Home Screen once the websocket connects.
Step 5 - Add the providers
Three providers go around the navigator: your own app context for the selected channel and thread, then Stream's OverlayProvider and Chat.
5.1 - Create the app context
Ideally, a context should store the current Channel and Thread selected by the user while moving through the app. Both are objects of the stream-chat client: the channel list gives you a Channel when the user picks a row, and the message list gives you a Thread when the user opens a message's replies.
Create AppContext.tsx:
import React, { PropsWithChildren, useState } from "react";
import type { Channel as ChannelType, Thread as ThreadType } from "stream-chat";
type AppContextType = {
channel: ChannelType | undefined;
setChannel: (channel: ChannelType | undefined) => void;
thread: ThreadType | undefined;
setThread: (thread: ThreadType | undefined) => void;
};
export const AppContext = React.createContext<AppContextType>({
channel: undefined,
setChannel: () => {},
thread: undefined,
setThread: () => {},
});
export const AppProvider = ({ children }: PropsWithChildren) => {
const [channel, setChannel] = useState<ChannelType>();
const [thread, setThread] = useState<ThreadType>();
return (
<AppContext.Provider value={{ channel, setChannel, thread, setThread }}>
{children}
</AppContext.Provider>
);
};
export const useAppContext = () => React.useContext(AppContext);To use the context, wrap the default component in App.tsx with AppProvider:
import { AppProvider } from "./AppContext";
// ...rest of the code
export default function App() {
return (
<SafeAreaProvider>
<StatusBar barStyle="dark-content" />
<AppProvider>
<GestureHandlerRootView style={styles.container}>
<NavigationStack />
</GestureHandlerRootView>
</AppProvider>
</SafeAreaProvider>
);
}5.2 - Add the OverlayProvider
The OverlayProvider is the highest level of the Stream Chat components and must be used near the root of your application, below SafeAreaProvider.
The OverlayProvider allows users to open the full-screen image viewer and message context menu as overlays on top of the rest of the application. You can go through the available props here.
Update the return statement in NavigationStack so the navigator is wrapped in OverlayProvider:
import {
// ... other imports
OverlayProvider,
useCreateChatClient,
} from "stream-chat-react-native";
// ...rest of the code
const NavigationStack = () => {
const chatClient = useCreateChatClient({
apiKey: chatApiKey,
userData: user,
tokenOrProvider: chatUserToken,
});
if (!chatClient) {
return (
<SafeAreaView>
<Text>Loading chat ...</Text>
</SafeAreaView>
);
}
return (
<NavigationContainer>
<OverlayProvider>
<Stack.Navigator>
<Stack.Screen component={HomeScreen} name="Home" />
</Stack.Navigator>
</OverlayProvider>
</NavigationContainer>
);
};If you see some errors at this point, please refer to our troubleshooting guide.
5.3 - Add the Chat component
The Chat component provides the chat client, translations, and theme to the rest of the SDK.
Update NavigationStack again so it wraps the navigator with Chat inside OverlayProvider:
import {
Chat,
OverlayProvider,
useCreateChatClient,
} from "stream-chat-react-native";
// ...rest of the code
const NavigationStack = () => {
const chatClient = useCreateChatClient({
apiKey: chatApiKey,
userData: user,
tokenOrProvider: chatUserToken,
});
if (!chatClient) {
return (
<SafeAreaView>
<Text>Loading chat ...</Text>
</SafeAreaView>
);
}
return (
<NavigationContainer>
<OverlayProvider>
<Chat client={chatClient}>
<Stack.Navigator>
<Stack.Screen component={HomeScreen} name="Home" />
</Stack.Navigator>
</Chat>
</OverlayProvider>
</NavigationContainer>
);
};Checkpoint: the app still renders the Home Screen with no errors, App is wrapped in AppProvider, and NavigationStack returns NavigationContainer -> OverlayProvider -> Chat -> Stack.Navigator.
Step 6 - Add the channel list
The ChannelList component renders the channels of a ChannelPaginator in a FlatList.
6.1 - Add the channel list screen
Before configuring this component, let's set up a screen for the channel list within the existing navigation stack. We will replace HomeScreen with ChannelListScreen.
// ...rest of the code
const ChannelListScreen = () => {
return null;
};
const NavigationStack = () => {
// ...existing chatClient logic
return (
<NavigationContainer>
<OverlayProvider>
<Chat client={chatClient}>
<Stack.Navigator>
<Stack.Screen
component={ChannelListScreen}
name="ChannelListScreen"
options={{ title: "Channels" }}
/>
</Stack.Navigator>
</Chat>
</OverlayProvider>
</NavigationContainer>
);
};6.2 - Display the channel list
A ChannelPaginator is the object behind a channel list. It holds the list's filters and sort order, queries the channels page by page, and keeps what it loaded. Your app creates it and registers it on client.channelManager, which keeps the list up to date as messages and other events arrive. ChannelList renders the paginator and loads its first page when it mounts.
The filters below show the messaging channels the current user is a member of, and sort puts the channel with the latest message first. Give every paginator an id of its own. Please check out Querying Channels for more information and various use cases of filters and sort, and Channel list paginators for how paginators work.
import { useEffect, useMemo } from "react";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import { ChannelList, useChatContext } from "stream-chat-react-native";
import { chatUserId } from "./chatConfig";
// ...rest of the code
const filters: ChannelFilters = {
members: { $in: [chatUserId] },
type: "messaging",
};
const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];
const ChannelListScreen = () => {
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 paginator={paginator} />;
};The useEffect registers the paginator when the screen mounts, and removes and disposes it when the screen unmounts. A paginator that is not registered shows its first page and then never changes. useChatContext gives the screen the client that Chat holds.
If you don't see any channels, the user is not a member of any yet. Seed one in Step 3, or drop the
membersfilter from thefiltersobject to list every messaging channel the user can read.
Checkpoint: the "Channels" screen lists the channels your user belongs to.


Step 7 - Channel screen
7.1 - Navigate to the channel screen
You can add the press handler for the list item within the ChannelList component using the onSelect prop. This is where you can add the logic for navigating to the channel screen, where we will render the message list and composer.
Let's implement the basic ChannelScreen component and logic for navigating from ChannelList to ChannelScreen.
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useAppContext } from "./AppContext";
// ...rest of the code
type RootStackParamList = {
ChannelListScreen: undefined;
ChannelScreen: undefined;
ThreadScreen: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
const ChannelScreen = () => {
return null;
};
const ChannelListScreen = ({
navigation,
}: NativeStackScreenProps<RootStackParamList, "ChannelListScreen">) => {
const { setChannel } = useAppContext();
const { client } = useChatContext();
// ...existing paginator logic
return (
<ChannelList
onSelect={(channel) => {
setChannel(channel);
navigation.navigate("ChannelScreen");
}}
paginator={paginator}
/>
);
};
const NavigationStack = () => {
// ...existing chatClient logic
return (
<NavigationContainer>
<OverlayProvider>
<Chat client={chatClient}>
<Stack.Navigator>
<Stack.Screen
component={ChannelListScreen}
name="ChannelListScreen"
options={{ title: "Channels" }}
/>
<Stack.Screen
component={ChannelScreen}
name="ChannelScreen"
options={{ title: "Channel" }}
/>
</Stack.Navigator>
</Chat>
</OverlayProvider>
</NavigationContainer>
);
};7.2 - Display the channel screen
The channel screen will comprise three main components:
MessageListcomponent used to render the list of messages sent in a channel.MessageComposercomponent used to render the input box needed to send messages, images, files, and commands to a channel.Channelcomponent that gives one channel to everything inside it. It also acts as a bridge between theMessageListandMessageComposercomponents.
The Channel component takes the channel as a prop. The MessageList and MessageComposer components don't need any props to be set, and we'll use the defaults set for these components.
Channel moves its content out of the keyboard's way, and the attachment picker needs to know how far up it may open. Pass the height of the navigation header, from useHeaderHeight(), as keyboardVerticalOffset and topInset. See Keyboard and insets.
import { useHeaderHeight } from "@react-navigation/elements";
import {
Channel,
MessageComposer,
MessageList,
} from "stream-chat-react-native";
// ...rest of the code
const ChannelScreen = () => {
const { channel } = useAppContext();
const headerHeight = useHeaderHeight();
if (!channel) {
return (
<SafeAreaView>
<Text>Loading chat ...</Text>
</SafeAreaView>
);
}
return (
<Channel
channel={channel}
keyboardVerticalOffset={headerHeight}
topInset={headerHeight}
>
<MessageList />
<MessageComposer />
</Channel>
);
};Checkpoint: tapping a channel opens it. You can send messages, long press a message to open the message menu and add a reaction, and use the built-in composer UI.


The default composer is text-only. Image and file attachments need the optional media packages - see the installation guide.
Step 8 - Thread screen
The Threads feature is similar to Slack's, which allows you to start a conversation about a particular message in a message list.
8.1 - Create the thread screen
Let's first set up a separate screen for the thread within our navigation stack.
// ...rest of the code
const ThreadScreen = () => null;
const NavigationStack = () => {
// ...existing chatClient logic
return (
<NavigationContainer>
<OverlayProvider>
<Chat client={chatClient}>
<Stack.Navigator>
<Stack.Screen
component={ChannelListScreen}
name="ChannelListScreen"
options={{ title: "Channels" }}
/>
<Stack.Screen
component={ChannelScreen}
name="ChannelScreen"
options={{ title: "Channel" }}
/>
<Stack.Screen
component={ThreadScreen}
name="ThreadScreen"
options={{ title: "Thread" }}
/>
</Stack.Navigator>
</Chat>
</OverlayProvider>
</NavigationContainer>
);
};8.2 - Navigate to the thread screen
As explained in the previous step, when a user long presses a message, it opens an overlay where the user can add a reaction and see many actions for the message.
MessageList accepts an onThreadSelect prop, which gets called with the Thread object when a user selects the "Thread Reply" action on the message overlay, or taps the replies of a message.
Update ChannelScreen so it stores the selected thread and navigates to ThreadScreen:
// ...rest of the code
const ChannelScreen = ({
navigation,
}: NativeStackScreenProps<RootStackParamList, "ChannelScreen">) => {
const { channel, setThread } = useAppContext();
const headerHeight = useHeaderHeight();
if (!channel) {
return (
<SafeAreaView>
<Text>Loading chat ...</Text>
</SafeAreaView>
);
}
return (
<Channel
channel={channel}
keyboardVerticalOffset={headerHeight}
topInset={headerHeight}
>
<MessageList
onThreadSelect={(thread) => {
setThread(thread);
navigation.navigate("ThreadScreen");
}}
/>
<MessageComposer />
</Channel>
);
};8.3 - Display the thread screen
The Thread component renders next to Channel, on a screen of its own, and not inside it. It takes the Thread object you stored as the thread prop, and renders the children you give it: a MessageList that shows the replies, with the parent message above them, and a MessageComposer that sends replies.
Thread takes the same configuration props as Channel, such as keyboardVerticalOffset and topInset. When it unmounts, it calls onThreadDismount, which clears the stored thread here.
import { Thread } from "stream-chat-react-native";
// ...rest of the code
const ThreadScreen = () => {
const { thread, setThread } = useAppContext();
const headerHeight = useHeaderHeight();
if (!thread) {
return (
<SafeAreaView>
<Text>Loading chat ...</Text>
</SafeAreaView>
);
}
return (
<Thread
keyboardVerticalOffset={headerHeight}
onThreadDismount={() => setThread(undefined)}
thread={thread}
topInset={headerHeight}
>
<MessageList />
<MessageComposer />
</Thread>
);
};Checkpoint: long press a message, choose "Thread Reply", and the Thread screen opens with the parent message, its own message list, and its own composer. Sending a reply keeps you in the thread.


Step 9 - Theming
So far you've used the SDK's default look. Theming in the React Native SDK is a plain object of React Native styles that is deep-merged with the defaults, so you only declare the keys you want to change. The DeepPartial<Theme> type keeps those keys type-checked.
Add a theme object to App.tsx and pass it to OverlayProvider through its value prop:
import type { DeepPartial, Theme } from "stream-chat-react-native";
// ...rest of the code
const chatTheme: DeepPartial<Theme> = {
semantics: {
chatBgOutgoing: "#81c784", // darker green - your own message bubbles
},
messageList: {
container: {
backgroundColor: "#c8e6c9", // light green - message list background
},
},
};
const NavigationStack = () => {
// ...existing chatClient logic
return (
<NavigationContainer>
<OverlayProvider value={{ style: chatTheme }}>
<Chat client={chatClient}>
<Stack.Navigator>
{/* ...rest of the navigator */}
</Stack.Navigator>
</Chat>
</OverlayProvider>
</NavigationContainer>
);
};semantics is the SDK's color token layer. Name every token you want recolored - overriding an upstream token such as accentPrimary does not cascade to the leaves.
Checkpoint: open a channel - the message list sits on a light green background and your own messages are a darker green. Messages from other people keep the SDK's default bubble color, because chatBgOutgoing only recolors your side; its mirror token is chatBgIncoming.


Read more about theming, including dark mode, in our theming documentation.
Step 10 - Customization
Theming changes tokens. To replace an actual view, use WithComponents and pass an overrides object - everything you don't name keeps its default.
import { WithComponents } from "stream-chat-react-native";
const overrides = { MessageItemView: MyCustomMessage };
<WithComponents overrides={overrides}>
<Channel channel={channel}>
<MessageList />
<MessageComposer />
</Channel>
</WithComponents>;WithComponents reads its overrides once, when it mounts, so declare the object outside of your components, as above.
WithComponents supports nesting - inner overrides merge over outer ones, so the closest provider wins. This lets you set app-wide defaults near the root and narrow overrides deeper in the tree (e.g. different components inside a thread screen).
10.1 - Customize the channel list
The ChannelList is essentially a FlatList of channels.
To customize the channel list item, override the ChannelPreview component using WithComponents. The default is ChannelPreviewView.
Objective: Highlight unread channels while keeping the default preview UI
Let's start by creating a custom list item component, which returns the default UI component ChannelPreviewView.
import {
ChannelPreviewView,
WithComponents,
type ChannelPreviewViewProps,
} from "stream-chat-react-native";
// ...rest of the code
const CustomListItem = (props: ChannelPreviewViewProps) => {
return <ChannelPreviewView {...props} />;
};
const overrides = { ChannelPreview: CustomListItem };
const ChannelListScreen = ({
navigation,
}: NativeStackScreenProps<RootStackParamList, "ChannelListScreen">) => {
const { setChannel } = useAppContext();
const { client } = useChatContext();
// ...existing paginator logic
return (
<WithComponents overrides={overrides}>
<ChannelList
onSelect={(channel) => {
setChannel(channel);
navigation.navigate("ChannelScreen");
}}
paginator={paginator}
/>
</WithComponents>
);
};The unread count of a channel arrives in the unread prop of the preview. We will use it to wrap unread channels in a light green background, and leave read channels as they are. The default preview has a transparent background, so the wrapper shows through.
import { StyleSheet, View } from "react-native";
// ...rest of the code
const CustomListItem = (props: ChannelPreviewViewProps) => {
const { unread } = props;
return (
<View style={unread ? styles.unreadContainer : undefined}>
<ChannelPreviewView {...props} />
</View>
);
};
const styles = StyleSheet.create({
// ...existing styles
unreadContainer: {
backgroundColor: "#c8e6c9", // light green
},
});Similarly, along with customizing the entire list item component, you can also override individual components within the list item, for example, ChannelPreviewStatus, ChannelPreviewAvatar, ChannelPreviewMessage, and ChannelPreviewUnreadCount by adding them to the overrides object. You can use the channel preview parts table to find out which components you can customize.


10.2 - Customize the message list
All components within the MessageList and MessageComposer can be customized using WithComponents.
Objective: Replace the Default Message UI with a Custom Component
The most common use case of customizing the MessageList is to have a custom UI for the message. You can do so by overriding MessageItemView via WithComponents as shown below.
const CustomMessage = () => {
return null;
};
const messageOverrides = { MessageItemView: CustomMessage };
const ChannelScreen = ({
navigation,
}: NativeStackScreenProps<RootStackParamList, "ChannelScreen">) => {
// ...existing logic
return (
<WithComponents overrides={messageOverrides}>
<Channel
channel={channel}
keyboardVerticalOffset={headerHeight}
topInset={headerHeight}
>
<MessageList
onThreadSelect={(thread) => {
setThread(thread);
navigation.navigate("ThreadScreen");
}}
/>
<MessageComposer />
</Channel>
</WithComponents>
);
};Now that we have configured the component, let's render the message on the UI. You can access the message object from the MessageContext. The MessageContext also gives you access to a boolean isMyMessage which you can use to style the message UI conditionally.
You can also access plenty of other useful properties and callbacks from this context, such as
setQuotedMessage,handleReaction, andonLongPress. Please check the MessageContext documentation for the full list.
import { useCallback } from "react";
import { Pressable } from "react-native";
import type { GestureResponderEvent } from "react-native";
import { useMessageContext } from "stream-chat-react-native";
// ...rest of the code
const CustomMessage = () => {
const { contextMenuAnchorRef, isMyMessage, message, onLongPress } =
useMessageContext();
const handleLongPress = useCallback(
(event: GestureResponderEvent) => onLongPress({ event }),
[onLongPress],
);
return (
<Pressable
onLongPress={handleLongPress}
ref={contextMenuAnchorRef}
style={isMyMessage ? styles.myMessage : styles.message}
>
<Text>{message.text}</Text>
</Pressable>
);
};
const styles = StyleSheet.create({
// ...existing styles
message: {
alignSelf: "flex-start",
backgroundColor: "#ededed",
borderRadius: 10,
margin: 10,
padding: 10,
width: "70%",
},
myMessage: {
alignSelf: "flex-end",
backgroundColor: "#ADD8E6",
borderRadius: 10,
margin: 10,
padding: 10,
width: "70%",
},
});This is a really simplified version of a custom message UI that displays only text. You can obviously add functionalities such as onPress, onLongPress handlers, and message actions according to your needs. Calling onLongPress from the context opens the message menu, which lines up with the view that holds contextMenuAnchorRef.
Generally, you wouldn't need to customize the entire message UI, but only the required parts such as MessageStatus, MessageAuthor, and MessageTimestamp. For this purpose, you can check the component customization guide to decide which key to pass in the overrides object. You can access MessageContext at every message-level component.
Checkpoint: unread channels in the list have a light green background behind the default preview, and messages render as your own bubbles - gray and left-aligned for others, blue and right-aligned for your own.


Verify the whole build
Step 10 replaced the message UI with a component that renders message.text and nothing else, so reactions, link previews and attachments do not show up on screen. Hand the SDK's default MessageItemView back to WithComponents before running the checks below:
import { MessageItemView } from "stream-chat-react-native";
// ...rest of the code
const messageOverrides = { MessageItemView };Your editor will flag CustomMessage as unused while the override is swapped out - that is expected.
yarn iosyarn androidThen confirm the full loop on the running app: the channel list loads, tap a channel, send a message, long press it for reactions, open a thread and reply, and paste a YouTube link to watch it unfurl. Run it on both platforms if you support both - the composer and keyboard handling are the parts most likely to differ.


Image and file uploads, voice recordings, richer video playback and offline storage are opt-in - each needs an extra package. See the installation guide to add them, then re-run this verification with attachments included.
Customize further - cookbooks
For larger changes, Stream has full cookbooks. Each one is a short, self-contained recipe that swaps out a single piece of the chat UI.
Channel list
- Preview UI - the pattern for customizing previews and list behavior
- Channel backgrounds
- Pinned and archived channels
- Channel header
Messages and composer
- Custom message and custom message actions
- Custom attachments and custom reactions
- Custom composer and emoji suggestions
- Voice recordings and audio playback - voice recording in the composer
- Livestream message list
Theming
- Theming - the full theming model, including dark mode
- Replacing the icon set and localization
- Providers and context hooks and overriding components
Beyond the basics
- Thread list UI and the threads view
- AI components - streaming AI responses in a channel
- Location sharing and flagging, muting, blocking and bans
- Read state, unread and receipts, keyboard and insets, and accessibility
- Performance
We have also demonstrated the power of the SDK by building open-source clones of popular chat applications such as WhatsApp, Slack, and iMessage. The source code for all of them is in the react-native-samples repository.
Troubleshooting
stream project is not initialized- CLI onboarding not run. Rungetstream initin the project directory first.- Stuck on "Loading chat ..." - the client never connected. Check the Metro logs; an auth error usually means the API key and token belong to different apps, and a websocket error usually means the device or emulator has no working internet connection.
token is invalid/ auth error - token minted for a different app, or expired. Re-mint withgetstream token <user-id>and confirm the API key matches.- Channel list is empty, no errors - the user has no channels. Seed one with
getstream api GetOrCreateChannel(Step 3), or create one in the dashboard with Chat Explorer. If you're using the built-in credentials, remove themembersfilter from thefiltersobject. - iOS build fails right after adding a dependency - native modules aren't linked.
yarn iosresolves pods for you; if you build from Xcode instead, runnpx pod-installfirst and rebuild. - A red screen mentions worklets or Reanimated, for example
Cannot read property 'bytecode' of undefined- the Babel plugin is missing. Add"react-native-worklets/plugin"tobabel.config.jsand restart withyarn start --reset-cache. - Gestures, swipes, or the message menu don't respond - the app root must be wrapped in
GestureHandlerRootView, as in Step 2. - Yellow warnings such as
react-native-haptic-feedback is not installed- in development builds, the SDK warns when an optional package is missing. Here they are haptic feedback and video playback. Install the package to turn its feature on, as the installation guide describes, or ignore the warning. - Errors about the old architecture - the SDK is New Architecture only. See the requirements in Installation.
- Stale code after an edit - clear the bundler cache with
yarn start --reset-cache, and rebuild the native app after any dependency change. - Crash mentioning safe area or gesture handler - provider order is wrong.
SafeAreaProvideris outermost, thenGestureHandlerRootView, then the Stream providers. - Thread screen opens empty -
threadwas never set. ConfirmonThreadSelectcallssetThread(thread)before navigating. - Theme has no effect - the theme goes on
OverlayProviderasvalue={{ style: chatTheme }}. A barevalue={chatTheme}is ignored. Keep the theme object defined outside the component so it isn't recreated on every render. - One theme key does nothing while the others work - at runtime, an unknown key is ignored without an error. Keep the
DeepPartial<Theme>annotation, so TypeScript flags a typo in your editor. Colors live undersemantics, and each leaf token has to be named explicitly. - A custom component never renders -
WithComponentsonly applies to its descendants. It has to wrap the component that renders the overridden view. - The keyboard covers the composer - pass
useHeaderHeight()to bothkeyboardVerticalOffsetandtopInsetonChannel. - TypeScript errors on your own custom data - the field is not declared in
custom-types.d.ts, or the file is outsideincludeintsconfig.json.
More platform-specific fixes live in the troubleshooting guide.
Next steps
- Push notifications - React Native push setup
- Offline support - cache channels and messages locally so the app works without a connection
- AI chat experiences - add an AI assistant to a channel with streaming responses: AI components
- Moderation - AI-powered content moderation for spam, profanity, and images
- Video and audio calls - the Video React Native SDK integrates with chat
- Design resources - download our free chat interface UI kit
- More platforms - the iOS, Android, and Expo tutorials
- Sample code - the fully featured sample app that ships with the SDK
Pricing is flexible with multiple tiers, including a free maker plan for hobby projects and small companies.
Final Thoughts
In this chat app tutorial, we built a fully functioning React Native messaging app with our SDK's component library. We started with the polished defaults, then customized them with a theme object and scoped component overrides - all with minimal code changes.
Both the chat SDK for React Native and the API have plenty of features available to support more advanced use-cases such as push notifications, content moderation, rich messages, and more.
For more information read the React Native SDK Overview documentation, or review Stream's API documentation if you want to build a more complex chat application.
Machine-readable resources
For AI agents and coding assistants working with this SDK:
- CLI + skills:
curl -fsSL https://getstream.io/cli.sh | bash, thengetstream skillsandgetstream skills stream-react-nativefor the React Native pack. (Alternative:npx skills add GetStream/agent-skills -s stream.) - Provisioning:
getstream init(auth + create/select org & app) ->getstream env --target expo(writes the public API key to.env; bare React Native CLI has no native target, so copy the value into your config) ->getstream token <user-id>(mint a token) - Data & config from the CLI:
getstream api <Endpoint> --request '{...}'for users, channels, and messages - React Native integration skill: invoke
/stream-react-nativein your agent for React Native CLI and Expo setup patterns;/stream-docssearches live SDK docs with citations - Docs index for LLMs:
https://getstream.io/chat/docs/sdk/react-native/llms.txt - Markdown endpoints: append
.mdto any docs URL for a clean, token-efficient version - Source of truth for APIs: the SDK repository - check the installed version's source rather than assuming APIs from training data

