How to build a realtime chat app in React Native

build-realtime-chat-app-react-native-header.png

In this tutorial, we’re going to explore creating a simple chat app in the React Native framework, which allows us to use the same source code to target both Android and iOS. In order to keep this example simple to follow we’re going to focus only on the basics – a single chat room, and \[…\]

Introduction

In this tutorial, we’re going to explore creating a simple chat app in the React Native framework, which allows us to use the same source code to target both Android and iOS. In order to keep this example simple to follow we’re going to focus only on the basics – a single chat room, and no authentication of the people chatting.

The application will work in two parts. The client application will receive events from Pusher informing it of new users and new messages, and there will be a server application that is responsible for sending message to Pusher.

In order to implement this you need to have the following on your computer:

  • A recent version of Node.js
  • A text editor

You will also need a mobile device with the Expo tools installed – available from the Android Play Store or the Apple App Store for free. This is used to test the React Native application whilst you are still developing it. It works by allowing you to start and host the application on your workstation, and connect to it remotely from your mobile device as long as you are on the same network.

Note as well that this article assumes some prior experience with writing JavaScript applications, and with the React framework – especially working with the ES6 and JSX versions of the language.

Creating a Pusher application to use

Firstly, we’ll need to create a Pusher application that we can connect our server and client to. This can be done for free here. When you create your application, you will need to make note of your App ID, App Key, App Secret, and Cluster:

Creating the server application

Our server application is going to be written in Node.js using the Express web framework. We are going to have three RESTful endpoints and no actual views. The endpoints are:

PUT /users/ – Indicate that a new user has joined DELETE /users/ – Indicate that a user has left POST /users//messages – Send a message to the chatroom

Creating a new Node application is done using the npm init call, as follows:

1> npm init
2    This utility will walk you through creating a package.json file.
3    It only covers the most common items and tries to guess sensible defaults.
4
5    See `npm help json` for definitive documentation on these fields
6    and exactly what they do.
7
8    Use `npm install <pkg> --save` afterwards to install a package and
9    save it as a dependency in the package.json file.
10
11    Press ^C at any time to quit.
12    name: (server) react-native-chat-server
13    version: (1.0.0)
14    description: Server component for our React Native Chat application
15    entry point: (index.js)
16    test command:
17    git repository:
18    keywords:
19    author:
20    license: (ISC)
21
22    {
23      "name": "react-native-chat-server",
24      "version": "1.0.0",
25      "description": "Server component for our React Native Chat application",
26      "main": "index.js",
27      "scripts": {
28        "test": "echo \"Error: no test specified\" && exit 1"
29      },
30      "author": "",
31      "license": "ISC"
32    }
33
34
35    Is this ok? (yes)

We then need to install the modules that we’re going to depend on – express, body-parser – to allow us to parse incoming JSON bodies – and pusher, to talk to the Pusher API.

1> npm install --save express body-parser pusher

This gives us everything we need to get our server application written. Because it’s so simple we can do it all in one file – index.js – which will look like this:

1const express = require('express');
2    const bodyParser = require('body-parser');
3    const Pusher = require('pusher');
4
5    const pusherConfig = require('./pusher.json'); // (1)
6    const pusherClient = new Pusher(pusherConfig);
7
8    const app = express(); // (2)
9    app.use(bodyParser.json());
10
11    app.put('/users/:name', function(req, res) { // (3)
12        console.log('User joined: ' + req.params.name);
13        pusherClient.trigger('chat_channel', 'join', {
14            name: req.params.name
15        });
16        res.sendStatus(204);
17    });
18
19    app.delete('/users/:name', function(req, res) { // (4)
20        console.log('User left: ' + req.params.name);
21        pusherClient.trigger('chat_channel', 'part', {
22            name: req.params.name
23        });
24        res.sendStatus(204);
25    });
26
27    app.post('/users/:name/messages', function(req, res) { // (5)
28        console.log('User ' + req.params.name + ' sent message: ' + req.body.message);
29        pusherClient.trigger('chat_channel', 'message', {
30            name: req.params.name,
31            message: req.body.message
32        });
33        res.sendStatus(204);
34    });
35
36    app.listen(4000, function() { // (6)
37        console.log('App listening on port 4000');
38    });

This is the entire Server application, which works as follows:

  • Create a new Pusher client and configure it to connect to our Pusher application, as configured above
  • Create a new Express server
  • Add a new route – PUT /users/:name. This will send a join message to the Pusher application with the name of the user that has joined as the payload
  • Add a new route – DELETE /users/:name. This will send a part message to the Pusher application with the name of the user that has just departed as the payload
  • Add a third route – POST /users/:name/messages. This will send a message message to the Pusher application with the name of the user sending the message and the actual message as the payload
  • Start the server listening on port 4000

Pusher has native support for Join and Leave notification as a part of its API, by leveraging the Presence Channel functionality. This requires authentication to be implemented before the client can use it though, which is beyond the scope of this tutorial, but will give a much better experience if you are already implementing authentication.


Note
Why the names join and part? It’s a throwback to the IRC specification. The names aren’t important at all – as long as they are distinct from each other, and consistent with what the client expects.


Before we can start the application, we need a Pusher configuration file. This goes in pusher.json and looks like this:

1{
2        "appId":"SOME_APP_ID",
3        "key":"SOME_APP_KEY",
4        "secret":"SOME_APP_SECRET",
5        "cluster":"SOME_CLUSTER",
6        "encrypted":true
7    }

The values used here are exactly the ones taken from the Pusher application config we saw above.

Starting the server
We can now start this up and test it out. Starting it up is simply done by executing index.js:

1> node index.js
2    App listening on port 4000

If we then use a REST Client to interact with the server, by sending the appropriate messages to our server.

Doing so will cause the correct messages to appear in the Debug Console in the Pusher Dashboard, proving that they are coming through correctly.

You can do the same for the other messages, and see how it looks:

Creating the client application

Our client application is going to be built using React Native, and leveraging the create-react-native-app scaffolding tool to do a lot of work for us. This first needs to be installed onto the system, as follows:

1> npm install -g create-react-native-app

Once installed we can then create our application, ready for working on:

1> create-react-native-app client
2    Creating a new React Native app in client.
3
4    Using package manager as npm with npm interface.
5    Installing packages. This might take a couple minutes.
6    Installing react-native-scripts...
7
8    npm WARN react-redux@5.0.6 requires a peer of react@^0.14.0 || ^15.0.0-0 || ^16.0.0-0 but none was installed.
9    Installing dependencies using npm...
10
11    npm WARN react-native-branch@2.0.0-beta.3 requires a peer of react@>=15.4.0 but none was installed.
12    npm WARN lottie-react-native@1.1.1 requires a peer of react@>=15.3.1 but none was installed.    
13
14    Success! Created client at client
15    Inside that directory, you can run several commands:
16
17      npm start
18        Starts the development server so you can open your app in the Expo
19        app on your phone.
20
21      npm run ios
22        (Mac only, requires Xcode)
23        Starts the development server and loads your app in an iOS simulator.
24
25      npm run android
26        (Requires Android build tools)
27        Starts the development server and loads your app on a connected Android
28        device or emulator.
29
30      npm test
31        Starts the test runner.
32
33      npm run eject
34        Removes this tool and copies build dependencies, configuration files
35        and scripts into the app directory. If you do this, you can’t go back!
36
37    We suggest that you begin by typing:
38
39      cd client
40      npm start
41
42    Happy hacking!

We can now start up the template application ensure that it works correctly. Starting it is a case of running npm start from the project directory:

Amongst other things, this shows a huge QR Code on the screen. This is designed for the Expo app on your mobile device to read in order to connect to the application. If we now load up Expo and scan this code with it, it will load the application for you to see:

Adding a Login screen
The first thing we’re going to need is a screen where the user can enter a name to appear as. This is simply going to be a label and a text input field for now.

To achieve this, we are going to create a new React component that handles this. This will go in Login.js and look like this:

1import React from 'react';
2    import { StyleSheet, Text, TextInput, KeyboardAvoidingView } from 'react-native';
3
4    export default class Login extends React.Component { // (1)
5      render() {
6        return (
7          <KeyboardAvoidingView style={styles.container} behavior="padding"> // (2)
8            <Text>Enter the name to connect as:</Text> // (3)
9            <TextInput autoCapitalize="none" // (4)
10                       autoCorrect={false}
11                       autoFocus
12                       keyboardType="default"
13                       maxLength={ 20 }
14                       placeholder="Username"
15                       returnKeyType="done"
16                       enablesReturnKeyAutomatically
17                       style={styles.username}
18                       onSubmitEditing={this.props.onSubmitName}
19                       />
20          </KeyboardAvoidingView>
21        );
22      }
23    }
24
25    const styles = StyleSheet.create({ // (5)
26      container: {
27        flex: 1,
28        backgroundColor: '#fff',
29        alignItems: 'center',
30        justifyContent: 'center'
31      },
32      username: {
33        alignSelf: 'stretch',
34        textAlign: 'center'
35      }
36    });

This works as follows:

  • Define our Login component that we are going to use
  • Render the KeyboardAvoidingView. This is a special wrapper that understands the keyboard on the device and shifts things around so that they aren’t hidden underneath it
  • Render some simple text as a label for the user
  • Render a text input field that will collect the name the user wants to register as. When the user presses the Submit button this will call a provided callback to handle the fact
  • Apply some styling to the components so that they look as we want them to

We then need to make use of this in our application. For now, this is a simple case of updating App.js as follows:

1import React from 'react';
2    import Login from './Login';
3
4    export default class App extends React.Component { // (1)
5      constructor(props) {
6        super(props); // (2)
7        this.handleSubmitName = this.onSubmitName.bind(this); // (3)
8        this.state = { // (4)
9          hasName: false
10        };
11      }
12
13      onSubmitName(e) { // (5)
14        const name = e.nativeEvent.text;
15        this.setState({
16          name,
17          hasName: true
18        });
19      }
20
21      render() { // (6)
22        return (
23          <Login onSubmitName={ this.handleSubmitName } />
24        );
25      }
26    }

This is how this works:

  • Define our application component
  • We need a constructor to set up our initial state, so we need to pass the props up to the parent
  • Add a local binding for handling when a name is submitted. This is so that the correct value for this is used
  • Set the initial state of the component. This is the fact that no name has been selected yet. We’ll be making use of that later
  • When a name is submitted, update the component state to reflect this
  • Actually render the component. This only renders the Login view for now

If you left your application running then it will automatically reload. If not then restart it and you will see it now look like this:

Managing the connection to Pusher
Once we’ve got the ability to enter a name, we want to be able to make use of it. This will be a Higher Order Component that manages the connection to Pusher but doesn’t render anything itself.

Firstly we are going to need some more modules to actually support talking to Pusher. For this we are going to use the pusher-js module, which has React Native support. This is important because React Native is not a full Node compatible environment, so the full pusher module will not work correctly.

1> npm install --save pusher-js

We then need our component that will make use of this. Write a file ChatClient.js:

1import React from 'react';
2    import Pusher from 'pusher-js/react-native';
3    import { StyleSheet, Text, KeyboardAvoidingView } from 'react-native';
4
5    import pusherConfig from './pusher.json';
6
7    export default class ChatClient extends React.Component {
8      constructor(props) {
9        super(props);
10        this.state = {
11          messages: []
12        };
13        this.pusher = new Pusher(pusherConfig.key, pusherConfig); // (1)
14
15        this.chatChannel = this.pusher.subscribe('chat_channel'); // (2)
16        this.chatChannel.bind('pusher:subscription_succeeded', () => { // (3)
17          this.chatChannel.bind('join', (data) => { // (4)
18            this.handleJoin(data.name);
19          });
20          this.chatChannel.bind('part', (data) => { // (5)
21            this.handlePart(data.name);
22          });
23          this.chatChannel.bind('message', (data) => { // (6)
24            this.handleMessage(data.name, data.message);
25          });
26        });
27
28        this.handleSendMessage = this.onSendMessage.bind(this); // (9)
29      }
30
31      handleJoin(name) { // (4)
32        const messages = this.state.messages.slice();
33        messages.push({action: 'join', name: name});
34        this.setState({
35          messages: messages
36        });
37      }
38
39      handlePart(name) { // (5)
40        const messages = this.state.messages.slice();
41        messages.push({action: 'part', name: name});
42        this.setState({
43          messages: messages
44        });
45      }
46
47      handleMessage(name, message) { // (6)
48        const messages = this.state.messages.slice();
49        messages.push({action: 'message', name: name, message: message});
50        this.setState({
51          messages: messages
52        });
53      }
54
55      componentDidMount() { // (7)
56        fetch(`${pusherConfig.restServer}/users/${this.props.name}`, {
57          method: 'PUT'
58        });
59      }
60
61      componentWillUnmount() { // (8)
62        fetch(`${pusherConfig.restServer}/users/${this.props.name}`, {
63          method: 'DELETE'
64        });
65      }
66
67      onSendMessage(text) { // (9)
68        const payload = {
69            message: text
70        };
71        fetch(`${pusherConfig.restServer}/users/${this.props.name}/messages`, {
72          method: 'POST',
73          headers: {
74            'Content-Type': 'application/json'
75          },
76          body: JSON.stringify(payload)
77        });
78      }
79
80      render() { // (10)
81        const messages = this.state.messages;
82
83        return (
84          <Text>Messages: { messages.length }</Text>
85        );
86      }
87    }

There’s an awful lot going on here, so let’s go over it all:

  • This is our Pusher client. The configuration for this is read from an almost identical to the one on the server – the only difference is that this file also has the URL to that server, but that’s not used by Pusher
  • This is where we subscribe to the Pusher channel that our Server is adding all of the messages to
  • This is a callback when the subscribe has been successful since it’s an asynchronous event
  • This is a callback registered whenever we receive a join message on the channel, and it adds a message to our list
  • This is a callback registered whenever we receive a part message on the channel, and it adds a message to our list
  • This is a callback registered whenever we receive a message message on the channel, and it adds a message to our list
  • When the component first mounts, we send a message to the server informing them of the user that has connected
  • When the component unmounts, we send a message to the server informing them of the user that has left
  • This is the handler for sending a message to the server, which will be hooked up soon
  • For now, we just render a counter of the number of messages received

This isn’t very fancy yet, but it already does all of the communications with both our server and with Pusher to get all of the data flow necessary.
Note that to communicate with our server we use the Fetch API, which is a standard part of the React Native environment. We do need the address of the server, which we put into our pusher.json file to configure it. This file then looks as follows here:

1{
2        "appId":"SOME_APP_ID",
3        "key":"SOME_APP_KEY",
4        "secret":"SOME_APP_SECRET",
5        "cluster":"SOME_CLUSTER",
6        "encrypted":true,
7        "restServer":"http://192.168.0.15:4000"
8    }

Note
When you actually deploy this for real, the restServer property will need to be changed to the address of the live server.


Chat Display
The next thing that we need is a way to display all of the messages that happen in our chat. This will be a list containing every message, displaying when people join when they leave and what they said. This will look like this:

1import React from 'react';
2    import { StyleSheet, Text, TextInput, FlatList, KeyboardAvoidingView } from 'react-native';
3    import { Constants } from 'expo';
4
5    export default class ChatView extends React.Component {
6      constructor(props) {
7        super(props);
8
9        this.handleSendMessage = this.onSendMessage.bind(this);
10      }
11
12      onSendMessage(e) { // (1)
13        this.props.onSendMessage(e.nativeEvent.text);
14        this.refs.input.clear();
15      }
16
17      render() { // (2)
18        return (
19          <KeyboardAvoidingView style={styles.container} behavior="padding">
20            <FlatList data={ this.props.messages } 
21                      renderItem={ this.renderItem }
22                      styles={ styles.messages } />
23            <TextInput autoFocus
24                       keyboardType="default"
25                       returnKeyType="done"
26                       enablesReturnKeyAutomatically
27                       style={ styles.input }
28                       blurOnSubmit={ false }
29                       onSubmitEditing={ this.handleSendMessage }
30                       ref="input"
31                       />
32          </KeyboardAvoidingView>
33        );
34      }
35
36      renderItem({item}) { // (3)
37        const action = item.action;
38        const name = item.name;
39
40        if (action == 'join') {
41            return <Text style={ styles.joinPart }>{ name } has joined</Text>;
42        } else if (action == 'part') {
43            return <Text style={ styles.joinPart }>{ name } has left</Text>;
44        } else if (action == 'message') {
45            return <Text>{ name }: { item.message }</Text>;
46        }
47      }
48    }
49
50    const styles = StyleSheet.create({
51      container: {
52        flex: 1,
53        backgroundColor: '#fff',
54        alignItems: 'flex-start',
55        justifyContent: 'flex-start',
56        paddingTop: Constants.statusBarHeight
57      },
58      messages: {
59        alignSelf: 'stretch'
60      },
61      input: {
62        alignSelf: 'stretch'
63      },
64      joinPart: {
65        fontStyle: 'italic'
66      }
67    });

This works as follows:

  • When the user submits a new message, we call the handler we were provided, and then clear the input box so that they can type the next message
  • Render a FlatList of messages, and an input box for the user to type their messages into. Each message is rendered by the renderItem function
  • Actually render the messages in the list into the appropriate components. Every message is in a Text component, with the actual text and the styling depending on the type of message.

We then need to update the render method of the ChatClient.js component to look as follows:

1render() {
2        const messages = this.state.messages;
3
4        return (
5            <ChatView messages={ messages } onSendMessage={ this.handleSendMessage } />
6        );
7      }

This is simply so that it renders our new ChatView in place of just the number of messages received.

Finally, we need to update our main view to display the Chat Client when logged in. Update App.js to look like this:

1render() {
2        if (this.state.hasName) {
3          return (
4            <ChatClient name={ this.state.name } />
5          );
6        } else {
7          return (
8            <Login onSubmitName={ this.handleSubmitName } />
9          );
10        }
11      }

The end result of this will look something like this:

Conclusion

This article has shown an introduction to the fantastic React Native framework for building universal mobile applications, and shown how it can be used in conjunction with the Pusher service for handling realtime messaging between multiple different clients.

All of the source code for this application is available at Github.