# Quick start

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

This page builds a chat app in four steps. Each step is a complete `App.tsx`. For a guided version, follow the [React Native Chat tutorial](https://getstream.io/chat/sdk/react-native/tutorial/cli/).

Before you start:

- Install the SDK, its peer dependencies and `stream-chat`, and configure the app, as described in [Installation](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/installation/).
- Create a Stream Chat app and [generate a token](https://getstream.io/chat/docs/react-native/tokens-and-authentication/) for a user. The examples use its API key, the user's ID and the token in place of `YOUR_API_KEY`, `YOUR_USER_ID` and `YOUR_USER_TOKEN`.

The samples import from `stream-chat-react-native`. In an Expo app, import the same names from `stream-chat-expo`.

## Create the client

```tsx title="App.tsx"
import { StyleSheet, Text } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import {
  Chat,
  OverlayProvider,
  useCreateChatClient,
} from "stream-chat-react-native";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  return (
    <SafeAreaProvider>
      <GestureHandlerRootView style={styles.container}>
        {client ? (
          <OverlayProvider>
            <Chat client={client}>
              <SafeAreaView
                edges={["top", "left", "right"]}
                style={styles.container}
              >
                <Text>The client is connected.</Text>
              </SafeAreaView>
            </Chat>
          </OverlayProvider>
        ) : (
          <Text>Connecting...</Text>
        )}
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 },
});

export default App;
```

`useCreateChatClient` creates a `StreamChat` client and connects the user. It returns `null` until the connection is open, and then the client. When the component unmounts, it disconnects the user.

`Chat` gives the client to every SDK component inside it. Your own components read it with `useChatContext()`. `OverlayProvider` hosts what the SDK draws over the whole screen, such as the message menu and the image gallery, so it goes above `Chat` and above your screens.

The hook creates a new client when `apiKey`, the user's `id` or `tokenOrProvider` changes. So a token provider function must not be re-created on every render: memoize it with `useCallback`, or declare it outside the component.

To configure the client and the objects it creates, pass an `options` object to `useCreateChatClient`. It takes the same tree as `client.config.set()`, and the hook reads it once, on the first render. This one turns on message drafts:

```tsx title="App.tsx" add="4"
const client = useCreateChatClient({
  apiKey,
  tokenOrProvider: userToken,
  options: { messageComposer: { drafts: { enabled: true } } },
  userData: { id: userId },
});
```

The settings and their defaults are on [Configuration](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/configuration/).

## Show one channel

A single channel needs no navigation. Get the channel from the client and pass it to `Channel`:

```tsx title="App.tsx"
import { useMemo } from "react";
import { StyleSheet, Text } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import type { StreamChat } from "stream-chat";
import {
  Channel,
  Chat,
  MessageComposer,
  MessageList,
  OverlayProvider,
  useCreateChatClient,
} from "stream-chat-react-native";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

const ChannelScreen = ({ client }: { client: StreamChat }) => {
  // The server creates the channel the first time it's watched.
  const channel = useMemo(
    () =>
      client.channel("messaging", "react-native-quick-start", {
        members: [{ user_id: userId }],
      }),
    [client],
  );

  return (
    // Nothing is above the channel, so the keyboard offset is 0.
    <Channel channel={channel} keyboardVerticalOffset={0}>
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};

const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  return (
    <SafeAreaProvider>
      <GestureHandlerRootView style={styles.container}>
        {client ? (
          <OverlayProvider>
            <Chat client={client}>
              <SafeAreaView
                edges={["top", "left", "right"]}
                style={styles.container}
              >
                <ChannelScreen client={client} />
              </SafeAreaView>
            </Chat>
          </OverlayProvider>
        ) : (
          <Text>Connecting...</Text>
        )}
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 },
});

export default App;
```

Here is what happens:

1. `client.channel("messaging", "react-native-quick-start", ...)` returns the object for that channel. The client keeps one object per channel, so a second call returns the same one.
2. `Channel` watches the channel when it mounts: it loads the latest messages, the members and the read state, and subscribes to the channel's events. The server creates the channel the first time. `Channel` also marks the channel as active while it's mounted, so the client reloads it after a reconnect, and it marks the channel read. See [Channel UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/channel-ui/).
3. `MessageList` renders the channel's messages, and `MessageComposer` writes new ones. A component inside `Channel` reads the channel with `useChannel()`.

The channel type, `messaging`, decides which features and permissions the channel has. See [channel types](https://getstream.io/chat/docs/react-native/channel-features/).

`MessageComposer` keeps itself above the bottom safe area, so the `SafeAreaView` leaves out the bottom edge. `Channel` resizes its content when the keyboard opens. `keyboardVerticalOffset` is the height of what sits above the channel, such as a navigation header. Pass it on every screen: without it, `Channel` uses a default sized for a header, 86.5 on iOS and -300 on Android. Here nothing is above the channel, so it's 0.

Without a place to show a thread, the reply button on a message does nothing. The next steps add navigation, a channel list and a thread screen.

## Add a channel list with navigation

The SDK doesn't navigate. It renders a channel list, calls back when the user picks a channel, and your navigator shows the channel screen. This step uses [React Navigation](https://reactnavigation.org/), with its native stack:

**RN CLI**

```bash title="Terminal"
yarn add @react-navigation/native @react-navigation/native-stack @react-navigation/elements react-native-screens
```

**Expo**

```bash title="Terminal"
npx expo install @react-navigation/native @react-navigation/native-stack @react-navigation/elements react-native-screens
```

Channels are queried from the server and kept in lists, matching filters and a sort order, by controllers called `ChannelPaginator`. Your app creates the paginator and registers it on `client.channelManager`, which keeps the list up to date as messages and other events arrive. `ChannelList` renders it, and loads its first page when it mounts.

```tsx title="App.tsx"
import { useEffect, useMemo } from "react";
import { StyleSheet, Text } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { useHeaderHeight } from "@react-navigation/elements";
import { NavigationContainer } from "@react-navigation/native";
import {
  createNativeStackNavigator,
  type NativeStackScreenProps,
} from "@react-navigation/native-stack";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import {
  Channel,
  ChannelList,
  Chat,
  MessageComposer,
  MessageList,
  OverlayProvider,
  useChatContext,
  useCreateChatClient,
} from "stream-chat-react-native";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

// The user's messaging channels, latest message first.
const filters: ChannelFilters = {
  members: { $in: [userId] },
  type: "messaging",
};
const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];

type StackParamList = {
  ChannelList: undefined;
  Channel: { cid: string };
};

const Stack = createNativeStackNavigator<StackParamList>();

const ChannelListScreen = ({
  navigation,
}: NativeStackScreenProps<StackParamList, "ChannelList">) => {
  const { client } = useChatContext();

  // One paginator per client. The channel list is the first screen of the stack, so it
  // stays mounted, and keeps its paginator, until the client goes away.
  const paginator = useMemo(
    () => new ChannelPaginator({ client, filters, id: "my-channels", sort }),
    [client],
  );

  // Registers the list on the channel manager, which keeps it up to date. Removes and
  // disposes it when the client goes away.
  useEffect(() => {
    client.channelManager.insertPaginator({ paginator });
    return () => {
      client.channelManager.removePaginator(paginator);
      paginator.dispose();
    };
  }, [client, paginator]);

  return (
    <ChannelList
      onSelect={(channel) =>
        navigation.navigate("Channel", { cid: channel.cid })
      }
      paginator={paginator}
    />
  );
};

const ChannelScreen = ({
  route,
}: NativeStackScreenProps<StackParamList, "Channel">) => {
  const { client } = useChatContext();
  const headerHeight = useHeaderHeight();
  // The list put the channel in the client, so it can be looked up by its cid.
  const channel = client.channelManager.get(route.params.cid);

  if (!channel) return null;

  return (
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      topInset={headerHeight}
    >
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};

const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  return (
    <SafeAreaProvider>
      <GestureHandlerRootView style={styles.container}>
        {client ? (
          <OverlayProvider>
            <Chat client={client}>
              <NavigationContainer>
                <Stack.Navigator>
                  <Stack.Screen
                    component={ChannelListScreen}
                    name="ChannelList"
                  />
                  <Stack.Screen component={ChannelScreen} name="Channel" />
                </Stack.Navigator>
              </NavigationContainer>
            </Chat>
          </OverlayProvider>
        ) : (
          <Text>Connecting...</Text>
        )}
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 },
});

export default App;
```

Here is what happens when the user taps a channel:

1. `ChannelList` renders a row for each channel in the paginator. Tapping a row calls `onSelect` with the channel.
2. `onSelect` navigates to the channel screen with the channel's cid. Route params should hold values that can be saved, such as strings, rather than objects.
3. The channel screen looks the channel up with `client.channelManager.get(cid)`. It's the same object the list holds. A screen opened from a push notification or a deep link, before any list loaded the channel, gets it with `client.channel(type, id)` instead.
4. `keyboardVerticalOffset` moves the composer above the keyboard by the height of the navigation header, and `topInset` keeps the attachment picker below the header.

The paginator has to be registered to receive events: an unregistered list shows its first page and then never changes. `ChannelList` never disposes the paginator, so the app does, once nothing shows it: here, when the user logs out and the client goes away. A paginator that lives above the navigator, for example in a context, keeps its channels when the list's screen unmounts, and the list doesn't query again when it mounts. While no `ChannelList` is mounted, though, the channel manager doesn't follow events, so hold its subscriptions yourself with `client.channelManager.registerSubscriptions()` if the list must stay current; see [Navigation](https://getstream.io/chat/docs/sdk/react-native/v10/ui/layout/navigation/).

## Add threads

Threads open in two places: from a message, when the user opens its replies, and from a thread list that shows the user's threads. This step adds both:

```tsx title="App.tsx"
import { useEffect, useMemo } from "react";
import { Button, StyleSheet, Text } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { useHeaderHeight } from "@react-navigation/elements";
import { NavigationContainer, useIsFocused } from "@react-navigation/native";
import {
  createNativeStackNavigator,
  type NativeStackScreenProps,
} from "@react-navigation/native-stack";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import {
  Channel,
  ChannelList,
  Chat,
  MessageComposer,
  MessageList,
  OverlayProvider,
  Thread,
  ThreadList,
  useChatContext,
  useCreateChatClient,
} from "stream-chat-react-native";

const apiKey = "YOUR_API_KEY";
const userId = "YOUR_USER_ID";
const userToken = "YOUR_USER_TOKEN";

const filters: ChannelFilters = {
  members: { $in: [userId] },
  type: "messaging",
};
const sort: SortParamRequest[] = [{ direction: -1, field: "last_message_at" }];

type StackParamList = {
  ChannelList: undefined;
  Channel: { cid: string };
  Thread: { threadId: string };
  ThreadList: undefined;
};

const Stack = createNativeStackNavigator<StackParamList>();

const ChannelListScreen = ({
  navigation,
}: NativeStackScreenProps<StackParamList, "ChannelList">) => {
  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]);

  useEffect(() => {
    navigation.setOptions({
      headerRight: () => (
        <Button
          onPress={() => navigation.navigate("ThreadList")}
          title="Threads"
        />
      ),
    });
  }, [navigation]);

  return (
    <ChannelList
      onSelect={(channel) =>
        navigation.navigate("Channel", { cid: channel.cid })
      }
      paginator={paginator}
    />
  );
};

const ChannelScreen = ({
  navigation,
  route,
}: NativeStackScreenProps<StackParamList, "Channel">) => {
  const { client } = useChatContext();
  const headerHeight = useHeaderHeight();
  const channel = client.channelManager.get(route.params.cid);

  if (!channel) return null;

  return (
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      topInset={headerHeight}
    >
      <MessageList
        onThreadSelect={(thread) =>
          navigation.navigate("Thread", { threadId: thread.id })
        }
      />
      <MessageComposer />
    </Channel>
  );
};

const ThreadScreen = ({
  route,
}: NativeStackScreenProps<StackParamList, "Thread">) => {
  const { client } = useChatContext();
  const headerHeight = useHeaderHeight();
  // The client keeps the thread that was opened, so it can be looked up by its id.
  const thread = client.threads.get(route.params.threadId);

  if (!thread) return null;

  return (
    <Thread
      keyboardVerticalOffset={headerHeight}
      thread={thread}
      topInset={headerHeight}
    >
      <MessageList />
      <MessageComposer />
    </Thread>
  );
};

const ThreadListScreen = ({
  navigation,
}: NativeStackScreenProps<StackParamList, "ThreadList">) => {
  // The list loads its threads, or catches up on new ones, whenever its screen comes into focus.
  const isFocused = useIsFocused();

  return (
    <ThreadList
      isFocused={isFocused}
      onThreadSelect={(thread) =>
        navigation.navigate("Thread", { threadId: thread.id })
      }
    />
  );
};

const App = () => {
  const client = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: { id: userId },
  });

  return (
    <SafeAreaProvider>
      <GestureHandlerRootView style={styles.container}>
        {client ? (
          <OverlayProvider>
            <Chat client={client}>
              <NavigationContainer>
                <Stack.Navigator>
                  <Stack.Screen
                    component={ChannelListScreen}
                    name="ChannelList"
                  />
                  <Stack.Screen component={ChannelScreen} name="Channel" />
                  <Stack.Screen component={ThreadScreen} name="Thread" />
                  <Stack.Screen
                    component={ThreadListScreen}
                    name="ThreadList"
                  />
                </Stack.Navigator>
              </NavigationContainer>
            </Chat>
          </OverlayProvider>
        ) : (
          <Text>Connecting...</Text>
        )}
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 },
});

export default App;
```

What changed:

- `onThreadSelect` on `MessageList` is called with a `Thread` object when the user opens a message's replies. The screen navigates with the thread's id, which is the id of its parent message.
- `Thread` renders next to the channel, on a screen of its own, not inside `Channel`. It takes the `Thread` object and gives it to the components inside it, so `MessageList` shows the replies, with the parent message above them, and `MessageComposer` sends replies. It takes the same configuration props as `Channel`, such as `keyboardVerticalOffset`.
- The thread screen looks the thread up with `client.threads.get(threadId)`. The client keeps a thread it opened for the rest of the session.
- `ThreadList` shows the user's threads, and calls `onThreadSelect` when one is tapped. `isFocused` tells it whether its screen is on top. Each time it comes into focus, the list loads its first page, or reloads to show threads with replies that arrived while it was hidden.

## Next steps

- [How the SDK is built](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/how-the-sdk-is-built/): What happens in the state and UI layers when a user opens a channel and sends a message.
- [Channel list paginators](https://getstream.io/chat/docs/sdk/react-native/v10/state/loading-data/channel-list-paginators/): Several channel lists, each with its own filters.
- [Configuration](https://getstream.io/chat/docs/sdk/react-native/v10/state/state-and-entities/configuration/): Configure the objects the SDK builds through client.config.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/quick-start/](https://getstream.io/chat/docs/sdk/react-native/v10/getting-started/quick-start/).