Build a Cross-Platform Messaging App with React Native Expo
Tags: reactnative, chatmessaging, ios, android
Published: 2024-04-17 07:21:11

Building a complete chat messaging app like WhatsApp can take time and effort. There are so many aspects developers need to consider to provide a full and feature-rich messaging experience, such as offline support, media attachment, and more. This tutorial uses [Stream Chat](https://github.com/GetStream/stream-chat-react-native/tree/develop) from React Native Directory and the [Expo framework](https://expo.dev/) to create a fully featured cross-platform mobile messaging experience.


## Tools and Technologies To Get Started 

After following the steps in this tutorial to build the sample project, we will test it on an iPhone and an actual Android device. Since we are using the Expo framework to initialize the React Native project, it removes the need for platform-specific configurations in Xcode and Android Studio. Let’s begin by:

- Installing [VS Code](https://code.visualstudio.com/): For developing and running our cross-platform application.
- Installing the Expo Go app from the [App Store](https://apps.apple.com/fi/app/expo-go/id982107779) and [Google Play Store](https://play.google.com/store/apps/details?id=host.exp.exponent): To test the messaging app on an iPhone and Android using a QR Code generated from VS Code after running it.


## Test the Sample App

To run the sample project on your iOS and Android devices, you should first install the chat SDK and its dependencies. Install the Expo Go app for iOS and Android. Then, download and add the files in this [GitHub](https://github.com/GetStream/stream-tutorial-projects/tree/main/ReactNative/NativeExpoChat) repository to the root folder of the Expo project. For the files that come with the Expo project generation, replace their content with the ones in the GitHub repo. 


**iOS and Android Versions of the Sample App**

![iOS version of the app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hqzgi4bw1cz6fqibgr3o.png)

The image above represents the iOS version of the React Native app. You have noticed a chat channel list (contacts), messages view, media attachment, and reactions.

Similarly, the Android version shown below has the same features as the iOS counterpart. 


![Android version of the app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tw5p4fbxyiw5q7ct7qse.png)

## Choosing a React Native Chat SDK

There are several factors to consider when choosing a chat SDK for a React Native Project. Let's outline some key considerations when selecting a universal messaging SDK for your app.

- **Offline Support**: Users should be able to browse contact lists and send and receive messages offline.
- **React Native CLI Support**: The SDK should integrate seamlessly with apps developers create with React Native CLI. 
- **Expo Support**: If you prefer initializing your React Native projects with popular frameworks like [Expo](https://expo.dev/), you should be able to implement the chat SDK in an Expo app with minimal effort.
- **React Native Directory Score**: The chat SDK you choose for your project should be one of the recommended chat libraries in the [React Native directory](https://reactnative.directory/?search=chat). 
- **Regular updates**: On [React Native Directory](https://reactnative.directory/?search=chat), you should check whether the chat SDK has regular updates and higher monthly downloads. 

The points above make Stream's [Chat SDK](https://getstream.io/chat/sdk/react-native/) for React Native an excellent choice for our use case. Let's install it along with the required dependencies in the following sections to build our cross-platform messaging app. 



## Create a New Expo App and Install Core Dependencies

The Stream's Chat SDK for React Native provides two installation options. 
- [Using React Native CLI](https://getstream.io/blog/react-native-android-ios-setup/): Requires complicated command line configurations, Android Studio, and Xcode settings.
- Using Expo framework: Handles all the configurations mentioned above and provides seamless testing with its GO app on iPhone and Android devices. 

Let's go with the Expo approach so that we do not have to worry about performing any additional settings required to run the app. Launch your favorite command line tool, [Terminal](https://support.apple.com/en-bw/guide/terminal/welcome/mac) or [Warp](https://www.warp.dev/), and run the below command to initialize a new React Native Expo app. 

First, install [Node](https://nodejs.org/en) and run `npx create-expo-app NativeChat`. Here, we use `npx` with your Node installation to create a new React Native Expo app called **NativeChat**.

Now, we have a blank Expo project. Next, we can install the Stream Chat Expo SDK using the command below. 

`expo install stream-chat-expo`

The chat SDK also has the following dependencies. 

- [Flat List](https://www.npmjs.com/package/@stream-io/flat-list-mvcp): For maintaining visible content position of scrollable UIs.
- [Netinfo](https://github.com/react-native-netinfo/react-native-netinfo): Helps retrieve information about a network.
- [Expo File System](https://docs.expo.dev/versions/latest/sdk/filesystem/): Provides access to the file system on Android and iOS. It will enable us to attach files and media to messages.
- [Expo Image Manipulator](https://docs.expo.dev/versions/latest/sdk/imagemanipulator/): Allows image manipulation on Android and iOS.
- [Expo Image Picker](https://docs.expo.dev/versions/latest/sdk/imagepicker/): Provides access to select and pick images and videos in a device's Photos Library. 
- [Expo Media Library](https://docs.expo.dev/versions/latest/sdk/media-library/): Helps access a device's Media Library. 
- React Native [Gesture Handler](https://www.npmjs.com/package/react-native-gesture-handler): A gesture and touch system for React Native.
- [React Native Animated](https://github.com/software-mansion/react-native-reanimated): An animation library for React Native projects.
- [SVG](https://github.com/software-mansion/react-native-svg) support for React Native.

Open the project in VS Code and `cd` into the root folder **NativeChat**.  Go to the **Toolbar** and click **Terminal -> New Terminal** and run the following multi-command to install the peer dependencies outlined above. 

```shell
expo install @stream-io/flat-list-mvcp @react-native-community/netinfo expo-file-system expo-image-manipulator expo-image-picker expo-media-library react-native-gesture-handler react-native-reanimated react-native-svg
```


## Configure the App’s babel.config.js and App.js

In the project's root folder, find `babel.config.js` and add the following code snippet to register the React Native Reanimated plugin we installed previously `plugins: [ 'react-native-reanimated/plugin', ],`. The content of `babel.config.js` will look like this:

```javascript
module.exports = function(api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      'react-native-reanimated/plugin', // The plugin must be the last item
    ],
  };
};
```

Also, previously, we installed the React Native Gesture Handler. When the app launches, the gesture recognizer should be immediately available. The best location to add it is at the app's entry point, thus in `App.js`. Open `App.js` from the project's root folder and add the following to the `import` section. 

`import { GestureHandlerRootView } from 'react-native-gesture-handler';`



## Add Optional Expo Libraries

To improve media upload and document attachment in our messaging app, let's install the following components for the Expo SDK. **Note**: You may skip the installations below because they are optional. 

- [Expo Document Picker](https://docs.expo.dev/versions/latest/sdk/document-picker/): A library that helps users to pick documents from their devices.
- [Expo AV](https://docs.expo.dev/versions/latest/sdk/av/): Provides audio and video playback. 
- [Expo Haptics](https://docs.expo.dev/versions/latest/sdk/haptics/): Helps to access the sound and vibration engine on Android and iOS devices. 
- [Expo Sharing](https://docs.expo.dev/versions/latest/sdk/sharing/): Provides support for file sharing.
- [Expo Clipboard](https://docs.expo.dev/versions/latest/sdk/clipboard/): Provides the ability to copy text to the clipboard.

Use the following multi-command to install all the above Expo libraries. 

```shell
npm add xpo-document-picker expo-av expo-haptics expo-sharing expo-clipboard 
```

**Note**: You can also add them with `yarn`.


## Setup the Chat SDK for React Native

In this section, we will create a JavaScript object `chatClient`, to connect the Expo app to a user from the Stream Chat SDK's backend. The `chatClient` should be initialized with an API key for a production app. Sign up for a free [dashboard account](https://dashboard.getstream.io/dashboard/v2/organization) to get an API key if you do not have one yet. In this tutorial, we will use hard-coded user credentials just for testing. 

Follow the steps below to configure the chat SDK to work with our Expo app.

**Step 1: Store the User Credentials**

In the project's root folder, add a new JavaScript file, `chatConfig.js,` and add the following code snippet to store user credentials, such as API key, user ID, token, and name for the app. 

```javascript
// chatConfig.js

export const chatApiKey = 'dz5f4d5kzrue'
export const chatUserId = 'lively-wood-7';
export const chatUserToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibGl2ZWx5LXdvb2QtNyIsImV4cCI6MTcxMDUzMjQ0Nn0.KMkjl68QocLTO_B5E4jIw2s5VBmRLOIvFnZkYNQepmc';
export const chatUserName = 'lively';
```


**Step 2: Connect the User**

Create a new JavaScript file, `useChatClient.js,` and add the sample code below to connect the user to the Stream's backend infrastructure. 

```javascript
// useChatClient.js

import { useEffect, useState } from 'react';
import { StreamChat } from 'stream-chat';
import { chatApiKey, chatUserId, chatUserName, chatUserToken } from './chatConfig';

const user = {
  id: chatUserId,
  name: chatUserName,
};

const chatClient = StreamChat.getInstance(chatApiKey);

export const useChatClient = () => {
  const [clientIsReady, setClientIsReady] = useState(false);

  useEffect(() => {
    const setupClient = async () => {
      try {
        chatClient.connectUser(user, chatUserToken);
        setClientIsReady(true);

      } catch (error) {
        if (error instanceof Error) {
          console.error(`An error occurred while connecting the user: ${error.message}`);
        }
      }
    };

    // If the chat client has a value in the field `userID`, the user gets connected
    if (!chatClient.userID) {
      setupClient();
    }
  }, []);

  return {
    clientIsReady,
  };
};
```


**Step 3: Share Channel and Thread Data Between Screens**

```javascript

// AppContext.js

import React, { useState } from 'react';

export const AppContext = React.createContext({
  channel: null,
  setChannel: (channel) => {},
  thread: null,
  setThread: (thread) => {},
});

export const AppProvider = ({ children }) => {
  const [channel, setChannel] = useState();
  const [thread, setThread] = useState();

  return <AppContext.Provider value={{ channel, setChannel, thread, setThread }}>{children}</AppContext.Provider>;
};

export const useAppContext = () => React.useContext(AppContext);
```


**Step 4: Configure the Chat Screen Components in** `App.js`


![A preview of the final cross-platform messaging app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pkfze2bre61xkd228q4t.gif)

In this section, we configure all the chat screen components at the app's entry point, `App.js`. 
Let's use a step-by-step approach to configure the app screens' navigation, channels filtering and sorting, `ChannelListScreen`, `ChannelScreen`, UI overlays, and `ThreadsScreen`.

**1. Import Components and Modules in `App.js`**

```javascript
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Text } from 'react-native';
import { useChatClient } from './useChatClient';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { AppProvider } from "./AppContext";
import {
  Chat,
  ChannelList,
  OverlayProvider,
  Channel,
  MessageList,
  MessageInput,
  Thread,
} from 'stream-chat-expo'; // stream-chat-react-native Or stream-chat-expo
import { StreamChat } from 'stream-chat';
import { chatApiKey, chatUserId } from './chatConfig';
import { useAppContext } from "./AppContext";
```

Here, we will use the `useChatClient` to connect the user to the backend chat client and integrate it into the app's `NavigationStack`. The `GestureHandlerRootView` will make gestures available soon after the app launches. Also, the `AppProvider` will make the current stored channel and threads available in the navigation stack.


**2. Create a Stack Navigator, Filter, and Sort Channels** 

```javascript
const Stack = createStackNavigator();
const chatClient = StreamChat.getInstance(chatApiKey);

const filters = {
  members: {
    '$in': [chatUserId]
  },
};

const sort = {
  last_message_at: -1,
};
```

The code snippet above creates a navigation `Stack` for the channel list, channel, and thread screens, a `chatClient` instance with an API key, filters the channel to show members only, and sorts the channel by a last message.


**3. Configure the `ChannelListScreen` Component**

```javascript
const ChannelListScreen = () => {
  const { setChannel } = useAppContext();
  return (
    <ChannelList
      onSelect={(channel) => {
        const { navigation } = props;
        setChannel(channel);
        navigation.navigate('ChannelScreen');
      }}
      filters={filters}
      sort={sort}
    />
  );
}
```

To display the chat `ChannelList`, we wrap it in the `ChannelListScreen` component and use the `onSelect` function to show the channel screen when a user selects a channel. 


**4. Configure the `ChannelScreen` Component**

```javascript
const ChannelScreen = props => {
  const { navigation } = props;
  const { channel, setThread } = useAppContext();

  return (
    <Channel channel={channel}>
      <MessageList 
        onThreadSelect={(message) => {
          if (channel?.id) {
            setThread(message);
            navigation.navigate('ThreadScreen');
          }
        }}
      />
      <MessageInput />
    </Channel>
  );
}
```

The `ChannelScreen` is configured to display three components, chat `Channel`, list of messages `MessageList`, and message composer `MessageInput`.


**5. Configure the `ThreadsScreen`**

```javascript
const ThreadScreen = props => {
  const { channel, thread } = useAppContext();
  return (
    <Channel channel={channel} thread={thread} threadList>
      <Thread />
    </Channel>
  );
}
```
In the above code snippet, we wrap the `Thread` component with the `Channel` component to display the thread messages in the app


**6. Setup the `NavigationStack`**

```javascript
const NavigationStack = () => {
  const { clientIsReady } = useChatClient();

  if (!clientIsReady) {
    return <Text>Loading chat ...</Text>
}
```
The app's `NavigationStack` component wraps the `Chat` component and the `OverlayProvider` to communicate all components in the app to the `chatClient` and display the overlay components such as reactions view, fullscreen image preview, and attachment preview.


**7. Display UI Overlays**

```javascript
  return (
    <OverlayProvider>
      <Chat client={chatClient}>
        <Stack.Navigator>
        <Stack.Screen name="ChannelList" component={ChannelListScreen} />
        <Stack.Screen name="ChannelScreen" component={ChannelScreen} />
        <Stack.Screen name="ThreadScreen" component={ThreadScreen} />
        </Stack.Navigator>
      </Chat>
    </OverlayProvider>
  );
};
```

The `OverlayProvider` helps to react to messages with a long press gesture. Configure it in the app's root and outside the navigation stack to show reactions, fullscreen images, and attachment views.


**8. Display the `default` Component**

```javascript
export default () => {
  return (
    <AppProvider>
        <GestureHandlerRootView style={{ flex: 1 }}>
            <SafeAreaView style={{ flex: 1 }}>
                <NavigationContainer>
                    <NavigationStack />
                </NavigationContainer>
            </SafeAreaView>
        </GestureHandlerRootView>
    </AppProvider>
);
};
```

Finally, in this code snippet, we wrap the `default` component with the `AppProvider` and `GestureHandlerRootView` to use the `AppContext` and make gestures available immediately when the app launches.


**Putting It All Together**

Here are complete implementations of the above code snippets in the `App.js` file. 

```javascript

// App.js

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Text } from 'react-native';
import { useChatClient } from './useChatClient';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { AppProvider } from "./AppContext";
import {
  Chat,
  ChannelList,
  OverlayProvider,
  Channel,
  MessageList,
  MessageInput,
  Thread,
} from 'stream-chat-expo'; // stream-chat-react-native Or stream-chat-expo
import { StreamChat } from 'stream-chat';
import { chatApiKey, chatUserId } from './chatConfig';
import { useAppContext } from "./AppContext";

const Stack = createStackNavigator();
const chatClient = StreamChat.getInstance(chatApiKey);

const filters = {
  members: {
    '$in': [chatUserId]
  },
};

const sort = {
  last_message_at: -1,
};

const ChannelListScreen = () => {
  const { setChannel } = useAppContext();
  return (
    <ChannelList
      onSelect={(channel) => {
        const { navigation } = props;
        setChannel(channel);
        navigation.navigate('ChannelScreen');
      }}
      filters={filters}
      sort={sort}
    />
  );
}

const ChannelScreen = props => {
  const { navigation } = props;
  const { channel, setThread } = useAppContext();

  return (
    <Channel channel={channel}>
      <MessageList 
        onThreadSelect={(message) => {
          if (channel?.id) {
            setThread(message);
            navigation.navigate('ThreadScreen');
          }
        }}
      />
      <MessageInput />
    </Channel>
  );
}

const ThreadScreen = props => {
  const { channel, thread } = useAppContext();
  return (
    <Channel channel={channel} thread={thread} threadList>
      <Thread />
    </Channel>
  );
}

const NavigationStack = () => {
  const { clientIsReady } = useChatClient();

  if (!clientIsReady) {
    return <Text>Loading chat ...</Text>
}

  return (
    <OverlayProvider>
      <Chat client={chatClient}>
        <Stack.Navigator>
        <Stack.Screen name="ChannelList" component={ChannelListScreen} />
        <Stack.Screen name="ChannelScreen" component={ChannelScreen} />
        <Stack.Screen name="ThreadScreen" component={ThreadScreen} />
        </Stack.Navigator>
      </Chat>
    </OverlayProvider>
  );
};

export default () => {
  return (
    <AppProvider>
        <GestureHandlerRootView style={{ flex: 1 }}>
            <SafeAreaView style={{ flex: 1 }}>
                <NavigationContainer>
                    <NavigationStack />
                </NavigationContainer>
            </SafeAreaView>
        </GestureHandlerRootView>
    </AppProvider>
);
};
```


## Run the App on iOS

We have configured the Stream Chat SDK to work with our React Native Expo app and set up the required navigation and chat screens. To run the app on an iOS device, you can use the **Expo Go app**. First, run the following command by navigating to the app's root folder and opening a new **Terminal** in VS Code. 

`npx expo start`

This command starts the Metro Server and shows a QR code with instructions on how to run the app on iOS, Android, and the web. 


![A preview of the Metro server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rb5qhet2voja20zq9h7b.png)

Ensure you have installed the Expo Go app on your iPhone or iPad. Launch the default **Camera app** for iOS and scan the QR code you see after running `npx expo start`. You will see a button to open the project with the Expo Go app. 
Following the steps above will launch the chat channel list screen. You can tap any channel list items to enter the messages screen to start chatting, add images and videos to chats, and show how you feel with reactions. 


![A preview of message reactions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b9httr2pyzvvs584paat.gif)


## Run the App on Android

On Android, you should scan the QR code the Metro Server generates with the Expo Go app you have installed instead of the **Camera** app for Android. After the app launches, you can perform the functions explained in the iOS section. 


![A preview of the app on Android](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z7k77fnerkg4r37tfjfi.png)


## Where To Go From Here

You have followed this tutorial to create a fully working cross-platform messaging app for React Native using the Expo framework and Stream. Congratulations 👏. You can take what we just built and go further by customizing the chat [channel list](https://getstream.io/chat/docs/sdk/reactnative/guides/customize-channel-list/), [message list](https://getstream.io/chat/react-native-chat/tutorial/#customize-message-list), [message composer](https://getstream.io/chat/docs/sdk/reactnative/guides/message-input-customization/), and more. The app supports offline browsing. However, this feature is turned off by default in the SDK. Head to our documentation to learn more about enabling [offline support](https://getstream.io/chat/docs/sdk/reactnative/basics/offline-support/) with a single line of code. Also, you can refer to our GitHub [sample apps](https://github.com/GetStream/react-native-samples) to learn how to build iMessage, Slack, and WhatsApp clones.