Stream Chat

Tutorial: React Native Chat App

Learn how to use our React Native Chat SDK in this comprehensive step-by-step tutorial.

Build a mobile chat application similar to Facebook Messenger or Telegram using Stream’s React Native Chat SDK library. By the end of this tutorial, you will have a fully functioning mobile app with support rich messages, reactions, threads, image uploads and videos.

We are also going to show how easy it easy to make customizations to the React Native Chat components that ship with this library and their styling.

Setup

First of all we need to setup our new project, make sure you have Android Studio or XCode installed, you will need this to run the mobile app on a device or phone simulator. To keep things as simple as possible we are going to use Expo.

Make sure that you have a recent version of Node (10+) installed. If you are not sure, just type this in your terminal node --version

Let’s first install expo’s command line tools, feel free to replace npm with yarn if you prefer that over npm

      
npm install -g expo-cli
    

Run the following commands to create a new React Native project called "AwesomeChat"

      
expo init -t blank --name AwesomeChat
cd AwesomeChat
npm add stream-chat-expo react-navigation
    

Note: our SDK library works without Expo as well, more information is available on the Github documentation page.

To get all the chat functionality in this tutorial, you will nee to get a free 14 day trial of Chat. No credit card is required.

Add Stream Chat to your application

Stream Chat comes with fully functional UI components and makes it very simple to add a chat to your mobile app. Let’s start by adding a simple conversation chat screen.

Open App.js in your text editor of choice and make the following changes:

      
import React from "react";
import { View, SafeAreaView } from "react-native";
import { StreamChat } from "stream-chat";
import {
  Chat,
  Channel,
  MessageList,
  MessageInput,
} from "stream-chat-expo";

const chatClient = new StreamChat('f8wwud5et5jd');
const userToken =
  'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoibm9pc3ktc2hhZG93LTgifQ.Zjdn41qnS9Smsj87dggqZX0eqVx260YpsFmQTEGbaYs';

const user = {
  id: 'noisy-shadow-8',
  name: 'Noisy shadow',
  image:
    'https://stepupandlive.files.wordpress.com/2014/09/3d-animated-frog-image.jpg',
};

chatClient.setUser(user, userToken);

class ChannelScreen extends React.Component {
  render() {
    const channel = chatClient.channel("messaging", "noisy-shadow-8");
    channel.watch();

    return (
      <SafeAreaView>
        <Chat client={chatClient}>
          <Channel channel={channel}>
            <View style={{ display: "flex", height: "100%" }}>
              <MessageList />
              <MessageInput />
            </View>
          </Channel>
        </Chat>
      </SafeAreaView>
    );
  }
}

export default class App extends React.Component {
  render() {
    return <ChannelScreen />;
  }
}
    

With this code we know have a fully working chat mobile app running. The Chat component is responsible of handling API calls and keep a consistent shared state across all other children components.

To preview the mobile app, you will need to install the Expo app on your phone and connect to WiFi so that you are on the same network as your computer. If you have Android Studio or Xcode you can also preview the application using the built-in emulator.

      
npm start
    

This will start the React Native development server, you can leave it running, it will live reload your application when you make code changes.

Chat UI React Native components come with batteries included:

Example chat with React Native

Rich Messaging

The built-in MessageList and MessageInput components provide several rich interactions out of the box

example URL preview component

URL previews

Try copy/paste https://goo.gl/Hok8hp in a message.

Example User nmentions

User mentions

Built-in user mention and autocomplete in all your chat channels

Example Chat commands

Chat commands

Built-in chat commands like /giphy and custom commands allow you to create rich user experiences.

Example Image upload module

Image uploads

Upload images directly from your Camera Roll.

Multiple conversations

Most chat applications handle more than just one single conversation. Apps like Facebook Messenger, Whatsapp and Telegram allows you to have multiple one to one and group conversations.

Let’s find out how we can change our application chat screen to display the list of conversations and navigate between them.

First of all we need to add some basic navigation to our mobile app. We want to list all conversations and be able to go from one to another. Stacked navigation can handle this very well and is supported by the awesome react-navigation package that we installed earlier on.

In order to keep things easy to follow we are going to have all code App.js

      
import React, { PureComponent } from 'react';
import { View, SafeAreaView, Text } from 'react-native';
import { StreamChat } from 'stream-chat';
import {
  Chat,
  Channel,
  MessageList,
  MessageInput,
  ChannelPreviewMessenger,
  ChannelList,
} from 'stream-chat-expo';
import { createAppContainer, createStackNavigator } from 'react-navigation';

const chatClient = new StreamChat('f8wwud5et5jd');
const userToken =
  'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoibm9pc3ktc2hhZG93LTgifQ.Zjdn41qnS9Smsj87dggqZX0eqVx260YpsFmQTEGbaYs';

const user = {
  id: 'noisy-shadow-8',
  name: 'Noisy shadow',
  image:
    'https://stepupandlive.files.wordpress.com/2014/09/3d-animated-frog-image.jpg',
};

chatClient.setUser(user, userToken);

class ChannelListScreen extends PureComponent {
  static navigationOptions = () => ({
    headerTitle: (
      <Text style={{ fontWeight: 'bold' }}>Awesome Conversations</Text>
    ),
  });

  render() {
    return (
      <SafeAreaView>
        <Chat client={chatClient}>
          <View style={{ display: 'flex', height: '100%', padding: 10 }}>
            <ChannelList
              filters={{ type: 'messaging', members: { $in: ['noisy-shadow-8'] } }}
              sort={{ last_message_at: -1 }}
              Preview={ChannelPreviewMessenger}
              onSelect={(channel) => {
                this.props.navigation.navigate('Channel', {
                  channel,
                });
              }}
            />
          </View>
        </Chat>
      </SafeAreaView>
    );
  }
}

class ChannelScreen extends React.Component {
  static navigationOptions = ({ navigation }) => {
    const channel = navigation.getParam('channel');
    return {
      headerTitle: (
        <Text style={{ fontWeight: 'bold' }}>{channel.data.name}</Text>
      ),
    };
  };

  render() {
    const { navigation } = this.props;
    const channel = navigation.getParam('channel');

    return (
      <SafeAreaView>
        <Chat client={chatClient}>
          <Channel client={chatClient} channel={channel}>
            <View style={{ display: 'flex', height: '100%' }}>
              <MessageList />
              <MessageInput />
            </View>
          </Channel>
        </Chat>
      </SafeAreaView>
    );
  }
}

const RootStack = createStackNavigator(
  {
    ChannelList: {
      screen: ChannelListScreen,
    },
    Channel: {
      screen: ChannelScreen,
    },
  },
  {
    initialRouteName: 'ChannelList',
  },
);

const AppContainer = createAppContainer(RootStack);

export default class App extends React.Component {
  render() {
    return <AppContainer />;
  }
}
    

If you run your application now, you will see the first chat screen now shows a list of conversations, you can open each by tapping and go back to the list.

The ChannelList component retrieves the list of channels based on a custom query and ordering. In this case we are showing the list of channels the current user is a member and we order them based on the time they had a new message. ChannelList handles pagination and updates automatically out of the box when new channels are created or when a new message is added to a channel.

Note: you can also specify more complex queries to match your use cases. The filter prop accepts a MongoDB-like query.

Click on this button to see how ChannelList handles new messages:

Customize channel preview

Let’s see how we can change the channel preview of the ChannelList. We are going to add a small badge showing the count of unread messages for each channel.

The React Native Chat SDK library allows you to swap components easily without adding much boiler code. This also works when you have to change deeply nested components like the ChannelPreview or Message .

      
import React, { PureComponent } from 'react';
import { View, SafeAreaView, TouchableOpacity, Text } from 'react-native';
import { StreamChat } from 'stream-chat';
import {
  Avatar,
  Chat,
  Channel,
  MessageList,
  MessageInput,
  ChannelList,
  IconBadge,
} from 'stream-chat-expo';
import { createAppContainer, createStackNavigator } from 'react-navigation';
import truncate from 'lodash/truncate';

const chatClient = new StreamChat('f8wwud5et5jd');
const userToken =
  'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoibm9pc3ktc2hhZG93LTgifQ.Zjdn41qnS9Smsj87dggqZX0eqVx260YpsFmQTEGbaYs';

const user = {
  id: 'noisy-shadow-8',
  name: 'Noisy shadow',
  image:
    'https://stepupandlive.files.wordpress.com/2014/09/3d-animated-frog-image.jpg',
};

chatClient.setUser(user, userToken);

class CustomChannelPreview extends PureComponent {
  channelPreviewButton = React.createRef();

  onSelectChannel = () => {
    this.props.setActiveChannel(this.props.channel);
  };

  render() {
    const { channel } = this.props;
    const unreadCount = channel.countUnread();

    return (
      <TouchableOpacity
        style={{
          display: 'flex',
          flexDirection: 'row',
          borderBottomColor: '#EBEBEB',
          borderBottomWidth: 1,
          padding: 10,
        }}
        onPress={this.onSelectChannel}
      >
        <Avatar image={channel.data.image} size={40} />
        <View
          style={{
            display: 'flex',
            flexDirection: 'column',
            flex: 1,
            paddingLeft: 10,
          }}
        >
          <View
            style={{
              display: 'flex',
              flexDirection: 'row',
              justifyContent: 'space-between',
            }}
          >
            <Text
              style={{
                fontWeight: unreadCount > 0 ? 'bold' : 'normal',
                fontSize: 14,
                flex: 9,
              }}
              ellipsizeMode="tail"
              numberOfLines={1}
            >
              {channel.data.name}
            </Text>
            <IconBadge unread={unreadCount} showNumber>
              <Text
                style={{
                  color: '#767676',
                  fontSize: 11,
                  flex: 3,
                  textAlign: 'right',
                }}
              >
                {this.props.latestMessage.created_at}
              </Text>
            </IconBadge>
          </View>
        </View>
      </TouchableOpacity>
    );
  }
}

class ChannelListScreen extends PureComponent {
  static navigationOptions = () => ({
    headerTitle: (
      <Text style={{ fontWeight: 'bold' }}>Awesome Conversations</Text>
    ),
  });

  render() {
    return (
      <SafeAreaView>
        <Chat client={chatClient}>
          <View style={{ display: 'flex', height: '100%', padding: 10 }}>
            <ChannelList
              filters={{ type: 'messaging', members: { $in: ['noisy-shadow-8'] } }}
              sort={{ last_message_at: -1 }}
              Preview={CustomChannelPreview}
              onSelect={(channel) => {
                this.props.navigation.navigate('Channel', {
                  channel,
                });
              }}
            />
          </View>
        </Chat>
      </SafeAreaView>
    );
  }
}

class ChannelScreen extends React.Component {
  static navigationOptions = ({ navigation }) => {
    const channel = navigation.getParam('channel');
    return {
      headerTitle: (
        <Text style={{ fontWeight: 'bold' }}>{channel.data.name}</Text>
      ),
    };
  };

  render() {
    const { navigation } = this.props;
    const channel = navigation.getParam('channel');

    return (
      <SafeAreaView>
        <Chat client={chatClient}>
          <Channel client={chatClient} channel={channel}>
            <View style={{ display: 'flex', height: '100%' }}>
              <MessageList />
              <MessageInput />
            </View>
          </Channel>
        </Chat>
      </SafeAreaView>
    );
  }
}

const RootStack = createStackNavigator(
  {
    ChannelList: {
      screen: ChannelListScreen,
    },
    Channel: {
      screen: ChannelScreen,
    },
  },
  {
    initialRouteName: 'ChannelList',
  },
);

const AppContainer = createAppContainer(RootStack);

export default class App extends React.Component {
  render() {
    return <AppContainer />;
  }
}
    

If you click on the add message button now, you will see the number of unread messages going up as well.

Message Threads

Stream Chat supports message threads out of the box. Threads allows users to create sub-conversations inside the same channel.

Using threaded conversations is very simple and mostly a matter of plugging the Thread component with React Navigation.

We created a new chat screen component called ThreadScreen

We registered the new chat screen to navigation

We pass the onThreadSelect prop to MessageList and use that to navigate to ThreadScreen.

Now we can open threads and create new ones as well, if you long press a message you can tap on Reply and it will open the same ThreadScreen.

      
import React, { PureComponent } from 'react';
import { View, SafeAreaView, TouchableOpacity, Text } from 'react-native';
import { StreamChat } from 'stream-chat';
import {
  Chat,
  Channel,
  MessageList,
  MessageInput,
  ChannelList,
  Thread,
  ChannelPreviewMessenger,
  CloseButton,
} from 'stream-chat-expo';

import { createAppContainer, createStackNavigator } from 'react-navigation';

const chatClient = new StreamChat('f8wwud5et5jd');
const userToken =
  'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoibm9pc3ktc2hhZG93LTgifQ.Zjdn41qnS9Smsj87dggqZX0eqVx260YpsFmQTEGbaYs';

const user = {
  id: 'noisy-shadow-8',
  name: 'Noisy shadow',
  image:
    'https://stepupandlive.files.wordpress.com/2014/09/3d-animated-frog-image.jpg',
};

chatClient.setUser(user, userToken);

class ChannelListScreen extends PureComponent {
  static navigationOptions = () => ({
    headerTitle: <Text style={{ fontWeight: 'bold' }}>Channel List</Text>,
  });

  render() {
    return (
      <SafeAreaView>
        <Chat client={chatClient}>
          <View style={{ display: 'flex', height: '100%', padding: 10 }}>
            <ChannelList
              Preview={ChannelPreviewMessenger}
              filters={{ type: 'messaging', members: { $in: ['noisy-shadow-8'] } }}
              sort={{ last_message_at: -1 }}
              options={{}}
              onSelect={(channel) => {
                this.props.navigation.navigate('Channel', {
                  channel,
                });
              }}
            />
          </View>
        </Chat>
      </SafeAreaView>
    );
  }
}

class ChannelScreen extends PureComponent {
  static navigationOptions = ({ navigation }) => {
    const channel = navigation.getParam('channel');
    return {
      headerTitle: (
        <Text style={{ fontWeight: 'bold' }}>{channel.data.name}</Text>
      ),
    };
  };

  render() {
    const { navigation } = this.props;
    const channel = navigation.getParam('channel');

    return (
      <SafeAreaView>
        <Chat client={chatClient}>
          <Channel client={chatClient} channel={channel}>
            <View style={{ display: 'flex', height: '100%' }}>
              <MessageList
                onThreadSelect={(thread) => {
                  this.props.navigation.navigate('Thread', {
                    thread,
                    channel: channel.id,
                  });
                }}
              />
              <MessageInput />
            </View>
          </Channel>
        </Chat>
      </SafeAreaView>
    );
  }
}

class ThreadScreen extends PureComponent {
  static navigationOptions = ({ navigation }) => ({
    headerTitle: <Text style={{ fontWeight: 'bold' }}>Thread</Text>,
    headerLeft: null,
    headerRight: (
      <TouchableOpacity
        onPress={() => {
          navigation.goBack();
        }}
        style={{marginRight: 20}}
      >
        <CloseButton />
      </TouchableOpacity>
    ),
  });

  render() {
    const { navigation } = this.props;
    const thread = navigation.getParam('thread');
    const channel = chatClient.channel(
      'messaging',
      navigation.getParam('channel'),
    );

    return (
      <SafeAreaView>
        <Chat client={chatClient}>
          <Channel
            client={chatClient}
            channel={channel}
            thread={thread}
            dummyProp="DUMMY PROP"
          >
            <View
              style={{
                display: 'flex',
                height: '100%',
                justifyContent: 'flex-start',
              }}
            >
              <Thread thread={thread} />
            </View>
          </Channel>
        </Chat>
      </SafeAreaView>
    );
  }
}

const RootStack = createStackNavigator(
  {
    ChannelList: {
      screen: ChannelListScreen,
    },
    Channel: {
      screen: ChannelScreen,
    },
    Thread: {
      screen: ThreadScreen,
    },
  },
  {
    initialRouteName: 'ChannelList',
  },
);

const AppContainer = createAppContainer(RootStack);

export default class App extends React.Component {
  render() {
    return <AppContainer />;
  }
}
    

Custom message

Customizing how messages are rendered is another very common use-case that the SDK supports easily.

Replace the built-in message component with your own is done by passing it as a prop to one of the parent components (eg. Channel, ChannelList, MessageList).

Let’s make a very simple custom message component that uses a more compact layout for messages.

      
import React from "react";
import { View, SafeAreaView, Text } from "react-native";
import { StreamChat } from "stream-chat";
import {
  Chat,
  Channel,
  MessageList,
  MessageInput,
  MessageText,
} from "stream-chat-expo";

const chatClient = new StreamChat('f8wwud5et5jd');
const userToken =
  'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoibm9pc3ktc2hhZG93LTgifQ.Zjdn41qnS9Smsj87dggqZX0eqVx260YpsFmQTEGbaYs';

const user = {
  id: 'noisy-shadow-8',
  name: 'Noisy shadow',
  image:
    'https://stepupandlive.files.wordpress.com/2014/09/3d-animated-frog-image.jpg',
};

chatClient.setUser(user, userToken);

export class CustomMessageSimple extends React.PureComponent {
  render() {
    const {message} = this.props;
    return (
      <View>
        <Text>{message.user.name}:</Text>
        <MessageText
          message={message}
        />
      </View>
    );
  }
}

class ChannelScreen extends React.Component {
  render() {
    const channel = chatClient.channel("messaging", "noisy-shadow-8");
    channel.watch();

    return (
      <SafeAreaView>
        <Chat client={chatClient}>
          <Channel client={chatClient} channel={channel}>
            <View style={{ display: "flex", height: "100%" }}>
              <MessageList
                Message={CustomMessageSimple}
              />
              <MessageInput />
            </View>
          </Channel>
        </Chat>
      </SafeAreaView>
    );
  }
}

export default class App extends React.Component {
  render() {
    return <ChannelScreen />;
  }
}
    

Custom styles

Sometimes all you want to do is just make a few styling adjustments without rewriting entire components. React Native does not have anything like CSS or SCSS available but you are not completely out of luck :)

React Native SDK library uses styled-components and themes to simplify making style changes. Standard it uses the default theme that comes with the components but you can override specific parts of it.

Let’s look at two common use cases: overriding some theme-like styling and make a style change to a component deeply nested in the hierarchy.

Green Theme

If you want to make a global style update like updating colors of your avatars, links etc, you can update the theme’s colors. You can do this really easily like this:

      
const theme = {
  colors: {
    primary: 'green',
  },
};
<Chat style={theme}>
  // the rest of your app
</Chat>
    

This small theme change updates all the places where the primary color is used. Under the hood it uses the ThemeProvider component provided by styled-components and a buildTheme function that’s included with the library. This function merges your overrides with the default theme, you only need to provide the style changes/additions that you want to make.

Avatar icon shape

What if we want to also change the shape of avatars. By default avatars use a circular mask, let’s change that into a square with rounded corners:

      
const theme = {
  'avatar.image': 'border-radius: 6px',
  colors: {
    primary: 'green',
  },
};
<Chat style={theme}>
  // the rest of your app
</Chat>
    

If we go back to the code from step one and add our simple additions to the theme, our code would look like this:

      
import React from "react";
import { View, SafeAreaView } from "react-native";
import { StreamChat } from "stream-chat";
import {
  Chat,
  Channel,
  MessageList,
  MessageInput,
} from "stream-chat-expo";

const theme = {
  'avatar.image': 'border-radius: 6px',
  colors: {
    primary: 'green',
  },
};

const chatClient = new StreamChat('f8wwud5et5jd');
const userToken =
  'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoibm9pc3ktc2hhZG93LTgifQ.Zjdn41qnS9Smsj87dggqZX0eqVx260YpsFmQTEGbaYs';

const user = {
  id: 'noisy-shadow-8',
  name: 'Noisy shadow',
  image:
    'https://stepupandlive.files.wordpress.com/2014/09/3d-animated-frog-image.jpg',
};

chatClient.setUser(user, userToken);

class ChannelScreen extends React.Component {
  render() {
    const channel = chatClient.channel("messaging", "noisy-shadow-8");
    channel.watch();

    return (
      <SafeAreaView>
      <Chat client={chatClient} style={theme}>
          <Channel channel={channel}>
            <View style={{ display: "flex", height: "100%" }}>
              <MessageList />
              <MessageInput />
            </View>
          </Channel>
        </Chat>
      </SafeAreaView>
    );
  }
}

export default class App extends React.Component {
  render() {
    return <ChannelScreen />;
  }
}
    

Customizing a single component

Let's say you want to change just the style of the avatar inside the message component and not everywhere else. You can do that the same way we're setting the theme on <Chat />. If you want to change just the avatar inside the message component all you need to do is:

      
<Message
  message={data.message}
  readBy={readBy}
  groupStyles={['bottom']}
  editing={false}
  style={{ 'avatar.fallback': 'background-color: red;' }}
  {...data.channelContext}
/>;
    

To find the path to the exact location of the style you want to change we added a 'styles' tab to every component in our reference docs. In here you can also find the default values we set on the components.

Final thoughts and Next Steps

In this Chat App tutorial we built a fully functioning React Native Chat app with our React SDK component library. We also showed how easy it is to customize the behavior and the style of the React Native chat app components with minimal code changes.

Both the Chat SDK for react native and the Chat API have plenty more features available to support more advanced use-cases such as push notifications, content moderation, rich messages and more. If you need more support for web, check out our React Chat tutorial. If you want some inspiration for your app, download our free chat interface UI kit.