Building a chat app in React Native

Messaging apps are on the increase recently. The past few years have brought apps like WhatsApp, Telegram, Facebook Messenger, Slack etc. People seem to prefer chat-based applications because they allow for Realtime Interaction. They also add a personal touch to the experience.

In this tutorial, you will learn how to build a Realtime chat application using React Native and Ably.

React Native lets you build mobile apps using only JavaScript. It uses the same design as React, letting you compose a rich mobile UI from declarative components. To learn more about React Native, please visit here.

Ably’s global platform provides developers everything they need to create, deliver and manage complex projects. Ably solves the hardest parts so they don’t have to. Our 100% data delivery guarantee offers unique peace of mind to stream data between apps, websites, and servers anywhere on earth in milliseconds.

Step 1 – Create your Ably app and API key

To follow this tutorial, you will need an Ably account. Sign up for a free account if you don’t already have one.

Access to the Ably global messaging platform requires an API key for authentication. API keys exist within the context of an Ably application and each application can have multiple API keys so that you can assign different capabilities and manage access to channels and queues.

You can either create a new application for this tutorial, or use an existing one.

To create a new application and generate an API key:

  1. Log in to your Ably account dashboard
  2. Click the “Create New App” button
  3. Give it a name and click “Create app”
  4. Copy your private API key and store it somewhere. You will need it for this tutorial.

To use an existing application and API key:

  1. Select an application from “Your apps” in the dashboard
  2. In the API keys tab, choose an API key to use for this tutorial. The default “Root” API key has full access to capabilities and channels.
  3. Copy the Root API key and store it somewhere. You will need it for this tutorial.

    Copy API Key screenshot

Step 2 – Setting Up React Native

To get started first, you need to install the React Native CLI if you don’t already have it installed. To install the CLI, run:

npm install -g react-native-cli

After installing the CLI, it’s time to create your project. Open a terminal, create a new project called Ably-presence-publish-subscribe-react-native:

react-native init Ably_presence_publish_subscribe_react_native

Before we continue, let me explain why the project was named that way.

  1. Ably: We will use the Ably Realtime library for Realtime Interactions
  2. presence: We will cover the concept of Ably presence channels
  3. publish: We will cover the concept of publishing messages in Ably
  4. subscribe: We will cover the concept of subscribing to channels and events in Ably
  5. react-native: We will build the chat application using React-Native

Note: You can give your project any other name you wish to.

Wait until React Native does all its installations, then you can change directory into the new project and run the application:

#change directory to Ably_presence_publish_subscribe_react_native
cd Ably_presence_publish_subscribe_react_native
#run the application for android
react-native run-android
#run the application for ios
react-native run-ios

See this step in Github

If all goes well, you should see a screen that says “Welcome to React Native” come up on your emulator or device.

Next, you will install the required libraries for this app.

npm install ably express body-parser --save

In the above bash command, you installed 4 packages. Here is the explanation of what the four packages are used for:

  1. Express: This is a Node.js web framework which we’ll use to create our API.
  2. Body-parser: This library is used by Express to parse body requests.
  3. Ably: This library is the official Ably library for JavaScript

See this step in Github

Step 3 – Creating Our API

First, Create a new file called server.js, which serves as your API in our root folder and paste the following:

//require express
var express = require('express')
    //define app as in instance of express
var app = express()
//require body-parser
var bodyParser = require('body-parser')

//use bodyparser as a middle ware
app.use(bodyParser.json())

//ably realtime
const ably = require('ably').Realtime;
const ablyRealtime = new ably('XXX_API_KEY')
const channel = ablyRealtime.channels.get('ably-chat');

//set cors middleware
app.use((req, res, next)=> {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
});
//handle sending of message via ably
app.post('/send_message', (req, res)=> {
    var data = {
        user: req.body.name,
        msg: req.body.msg,
        action: req.body.action
    }
    channel.publish('data', data, (err)=> {
        if (err) {
            console.log('publish failed with error ' + err);
        } else {
            console.log('publish succeeded ' + data.msg);
        }
    })
    res.send({ status: 'okay', data: data });

});

//listen on port and serve the app
app.listen(3000, ()=> {
    console.log('Example app listening on port 3000!');
});

See this step in Github

The code block above is our Express API server setup.

At the beginning of the file, you had required Express, Body-parser and Ably libraries for Node.js respectively.
Next, you created a new Ably object, passing in your API Key.
Next, you set the CORS header to your request, using a middleware function.
Finally, you create a post handler for the / route, and we then make Ably publish to a channel called ably-chat with an event called message.
Note both the channel name and the event name used on this server. The channel name will be subscribed to while you will listen to the event in your React Native app.
This is all we need on the server side for your API call to work.

Next, start up your API by running:

node server.js

Step 3 – Building The React Native Application

To get started, create a new file called chat.js and add paste in the following:

import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native'
export default class Chat extends Component<{}> {
  render() {
    return (
      <View style={styles.container}>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5FCFF',
    paddingTop: 10,
  },
});

In the code above, nothing much is happening. It is a blank page which you will use to hold your chat component later on.

Next, Open App.js and replace content with:

import React, { Component } from "react";
import Chat from "./Chat";
import { StyleSheet, View, Text, TextInput, Button, AsyncStorage } from "react-native";

export default class App extends Component<{}> {
  constructor(props) {
    super(props);
    this.state = { txt: "", user: null };
    this.handler = this.handler.bind(this);
  }

  componentDidMount = () => {
    AsyncStorage.getItem("user")
      .then(value => {
        this.setState({ user: value });
      })
      .done();
  };
  submitName = () => {
    AsyncStorage.setItem("user", this.state.txt);
    this.setState({ user: this.state.txt });
  };

  handler = () => {
    this.setState({
      user: null
    });
  };
  getRender = () => {
    if (this.state.user != null) {
      return <Chat handler={this.handler} />;
    } else {
      return (
        <View style={styles.container}>
          <Text style={{ alignItems: "center" }}>
            Please Give us a name to join the chat with
          </Text>
          <View
            style={{
              flexWrap: "wrap",
              alignItems: "center",
              flexDirection: "row"
            }}
          >
            <TextInput
              value={this.state.txt}
              style={{ width: "80%" }}
              placeholder="Enter Your message!"
              onChangeText={txt => this.setState({ txt })}
            />

            <Button
              style={{ width: "20%" }}
              onPress={this.submitName}
              title="Send"
              color="#841584"
            />
          </View>
        </View>
      );
    }
  };

  render = ()=> {
    return this.getRender();
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#F5FCFF"
  }
});

See this step in Github

If you look at the code above, these are the differences between the original App.js file and this new one.

Above, you have imported React Native UI component (View, Text, Button, TextInput, StyleSheet) to help us with our UI styling. Also, you imported AsyncStorage, which you will use to store items on the device.

In the constructor, you declared a constructor passing in props, called the super constructor of the props. You declared two items in your state assignment namely txt and user. Finally, you made a new assignment called handler to a function named handler which you will define soon.

Next, you defined the componentDidMount method which React will fire once the component is mounted. In this method, you used the AsyncStorage component to check if a value exists and if it does, set the state of user to that value.

In the submitName method, you used AsyncStorage to set the value of user which you are checking for in the componentDidMount method to the value of the current txt state.

Just after the submitName method, you defined the handler method assigned in the constructor. In this method, you set the state of user to null. Why have we done this? Is it needed? You will find out in the next method called getRender

In the getRender function, you first check if the user state is not null, then return the Chat component which is still empty for now. If the user state is null, it means the user is yet to select a user name. If the user state is null, it presents them with a form to enter their user name.

Take a look at the form returned when there is no username, you will notice:

  1. The presence of a TextInput whose value is set to the txt state and the onChangeText event alters the txt state
  2. The presence of a Button whose onPress event fires the submitName event you had defined earlier.

If you run the app, you will be presented with a screen that shows a text input and button prompting you to set a username.


enter username screenshot

However, if you set a username now, you will be presented with an empty page, as you have not created the chat component yet. Open chat.js and replace the content with:

import React, { Component } from "react";
import { StyleSheet, View, AsyncStorage } from "react-native";

var Realtime = require("ably").Realtime;
var ably, channel;

export default class Chat extends Component<{}> {
  constructor(props) {
    super(props);
    this.state = {
      user: "",
      msg: [],
      txt: "",
      usersCount: 0
    };
  }

  componentDidMount = () => {
    AsyncStorage.getItem("user")
      .then(value => {
        this.setState({ user: value });
        this.subscribe();
      })
      .done();
  };

  render = () => {
    return (
      <View style={styles.container}>
      </View>
    );
  };
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#F5FCFF",
    paddingTop: 10
  };
});

See this step in Github

If you look at the code above, these are the differences between the former chat.js file and this new one.

Above, we imported AsyncStorage, which we will use to store and retrieve items on the device. Also, we required the Ably Realtime library and declared two new variables namely Ably and channel.

In the constructor, we passed in props, called the super constructor of the props. we then declared some items in the state assignment namely msg, txt, user and usersCount.

Next, we defined the componentDidMount method which React will fire once the component is mounted. In this method, we used the AsyncStorage component to check if a value exists and if it does, set the state of user to that value. Also, we called the subscribe function which we will treat soon.

Here is what our subscribe function looks like:

subscribe = () => {
  ably = new Realtime({
    key: "XXX_API_KEY",
    clientId: this.state.user
  });
  channel = ably.channels.get("ably-chat");
  channel.presence.subscribe("enter", member => {
    var data = {
      msg: "joined the chat",
      user: member.clientId,
      action: "joined"
    };

    var newmsg = this.state.msg;

    newmsg.push(data);

    channel.presence.get((err, members) => {
      this.setState({ msg: newmsg, usersCount: members.length });
    });
  });

  channel.presence.subscribe("leave", member => {
    var data = {
      msg: "left the chat",
      user: member.clientId,
      action: "left"
    };

    var newmsg = this.state.msg;

    newmsg.push(data);

    channel.presence.get((err, members) => {
      this.setState({ msg: newmsg, usersCount: members.length });
    });
  });

  channel.presence.enter();

  channel.subscribe("message", msg => {
    var newmsg = this.state.msg;

    newmsg.push(msg.data);

    this.setState({ msg: newmsg, txt: "" });
  });
};

See this step in Github

The subscribe method is where all Realtime subscriptions such as presence and subscribe are made. Let’s look at what goes on in this method.

  1. First, you set the Ably variable to an instance of the Realtime library passing in your API Key and the current user. The reason for putting in the current user is because you want to access presence events.
  2. You subscribed to a channel called ably-chat.
  3. You subscribed to enter presence events by registering a callback that is invoked whenever a new member has entered the channel. Also, you get the number of users in the chat room after a new user joins.
  4. You subscribed to the presence event of leave by registering a callback that is invoked whenever a member has left the channel. Also, you get the number of current users in the chat room after a user leaves the chat.
  5. You made a call to channel.presence.enter() which triggers the entry into the presence event of the channel been listened to.
  6. Next, a normal subscription is made to the message event which emits Realtime chat messages, and the message is added to the array of messages available and displayed in Realtime.

Next, we will treat a load_message function which does the actual display of our chat messages:


load_messages = () => {
  var chat = [];
  for (var i = 0; i < this.state.msg.length; i++) {
    if (this.state.msg[i].action == "joined") {
      chat.push(
        <Text style={{ padding: 3 }} key={i}>
          {this.state.msg[i].user} {this.state.msg[i].msg}
        </Text>
      );
    } else if (this.state.msg[i].action == "left") {
      chat.push(
        <Text style={{ padding: 3 }} key={i}>
          {this.state.msg[i].user} {this.state.msg[i].msg}
        </Text>
      );
    } else if (this.state.msg[i].user == this.state.user) {
      chat.push(
        <View
          key={i}
          style={{
            width: "auto",
            backgroundColor: "rgb(244, 226, 96)",
            alignSelf: "flex-start",
            padding: 10,
            borderRadius: 25,
            marginBottom: 5
          }}
          >
          <Text> {this.state.msg[i].msg}  </Text>
        </View>
      );
    } else {
      chat.push(
        <View key={i}>
          <View
            style={{
              width: "auto",
              backgroundColor: "rgb(125, 185, 232)",
              alignSelf: "flex-end",
              padding: 10,
              borderRadius: 25
            }}
            >
            <Text> {this.state.msg[i].msg}{" "}
            </Text>
          </View>
          <Text
            style={{
              width: "auto",
              alignSelf: "flex-end",
              paddingRight: 10,
              marginBottom: 5,
              fontSize: 6
            }}
            >
            {this.state.msg[i].user}
          </Text>
        </View>
      );
    }
  }
  return chat;
};

See this step in Github

In the load_message function, a lot of things are going on here, but what the function primarily does is to loop through the messages, check for the action of the messages, and apply either the chat style or notification style to the messages. For example, messages from the presence event leave and enter have the same styling, while messages from the message event have a chat bubble style. Also, the loop checks for the user who sent the message and floats the message to the appropriate position. For example, messages sent from you will be floated to the left, while other users messages would be floated to the right.

Please note that you need to change the import of components from React Native to contain the Text component used in the load_message function as seen below:

import { StyleSheet, View, Text, AsyncStorage } from "react-native";

Next, we define a function which handles submission of chat messages to our server:

submitChat = () => {
  if (this.state.txt != "") {
    fetch("XXX_API_ROUTE/send_message", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        msg: this.state.txt,
        name: this.state.user,
        action: "chat"
      })
    })
      .then(response => response.json())
      .then(responseJson => { })
      .catch(error => {
        console.error(error);
      });
  }
};

See this step in Github

Note: If you use IP address such as 127.0.0.1 or localhost, the request is most likely going to fail. This is because, in React Native, localhost or 127.0.0.1 refers to the internal application. Please use the network IP for your system instead To find the IP for your system, please visit this link.

In the submitChat function, you trigger a POST request to our API which in turns publishes the message to the right channel and event. You used the fetch API provided by React Native to send an AJAX request to our server which we created earlier.

Wouldn’t it be nice to allow users a chance to leave the chat room using a button? Yes, it will. Next, we will define a leave chat function:

leaveChat = () => {
  AsyncStorage.removeItem("user");
  channel.presence.leave();
  this.setState({ user: "" });
  this.props.handler();
};

See this step in Github

In the leaveChat function, you used the AsyncStorage remove method to remove the user key from the device, leave the presence event by calling channel.presence.leave(). Next, you set the user state to an empty string and also call the handler method which has been passed to the chat component as a prop from the App component.

Finally, let’s alter our render function to do some justice to our code:

  render = () => {
    chatMessages = this.load_messages();
    return (
      <View style={styles.container}>
        <View
          style={{
            alignItems: "center",
            flexDirection: "row",
            top: 0,
            marginBottom: 5
          }}
          >
          <Button
            style={{ width: "40%" }}
            onPress={this.leaveChat}
            title="Leave Chat"
            color="#841584"
            />
          <Text>{this.state.usersCount} member(s) active</Text>
        </View>
        <ScrollView>

          {chatMessages}

        </ScrollView>
        <View
          style={{
            flexWrap: "wrap",
            alignItems: "flex-start",
            flexDirection: "row",
            position: "absolute",
            bottom: 0
          }}
          >
          <TextInput
            value={this.state.txt}
            style={{ width: "80%" }}
            placeholder="Enter Your message!"
            disabled={this.state.user == ""}
            onChangeText={txt => this.setState({ txt }) }
            />

          <Button
            style={{ width: "20%" }}
            onPress={this.submitChat}
            title="Send"
            color="#841584"
            />
        </View>
      </View>
    );
  };
}

See this step in Github

In our render statement, the chatMessages variable was rendered, so it can display its content. Also, notice there is have a text input and a button.
The text input text is used to set the txt state any time the text changes using the onChangeText event of the button. Notice that our button also calls the submitChat function anytime it is pressed by binding it to its onPress function.

Please note that you need to change the import of components from React Native to contain the components used in the render function as seen below:

import { StyleSheet, Text, View, TextInput, Button, ScrollView, AsyncStorage } from "react-native";

Now, the application is completed.


Demo image

Conclusion

In this article, we have seen how to make a Realtime chat application in Android and iOS using React Native. We have secured the design choices important, to begin with, and the cases above ought to help you fill in the holes and give an outline of a portion of the other design choices accessible to you.
Hope this tutorial has helped you to get started with Ably and Mobile apps?