Build a chat app with React Native
A basic understanding of React and Node.js are needed to follow this tutorial.
Social chat applications are hugely popular these days, allowing people to stay connected on topics they are interested in from all over the world. In this article 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:
> npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help json` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
name: (server) react-native-chat-server
version: (1.0.0)
description: Server component for our React Native Chat application
entry point: (index.js)
test command:
git repository:
keywords:
author:
license: (ISC)
{
"name": "react-native-chat-server",
"version": "1.0.0",
"description": "Server component for our React Native Chat application",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
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.
> 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:
const express = require('express');
const bodyParser = require('body-parser');
const Pusher = require('pusher');
const pusherConfig = require('./pusher.json'); // (1)
const pusherClient = new Pusher(pusherConfig);
const app = express(); // (2)
app.use(bodyParser.json());
app.put('/users/:name', function(req, res) { // (3)
console.log('User joined: ' + req.params.name);
pusherClient.trigger('chat_channel', 'join', {
name: req.params.name
});
res.sendStatus(204);
});
app.delete('/users/:name', function(req, res) { // (4)
console.log('User left: ' + req.params.name);
pusherClient.trigger('chat_channel', 'part', {
name: req.params.name
});
res.sendStatus(204);
});
app.post('/users/:name/messages', function(req, res) { // (5)
console.log('User ' + req.params.name + ' sent message: ' + req.body.message);
pusherClient.trigger('chat_channel', 'message', {
name: req.params.name,
message: req.body.message
});
res.sendStatus(204);
});
app.listen(4000, function() { // (6)
console.log('App listening on port 4000');
});
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 it’s 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 article, 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:
{
"appId":"SOME_APP_ID",
"key":"SOME_APP_KEY",
"secret":"SOME_APP_SECRET",
"cluster":"SOME_CLUSTER",
"encrypted":true
}
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
:
> node index.js
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:
> npm install -g create-react-native-app
Once installed we can then create our application, ready for working on:
> create-react-native-app client
Creating a new React Native app in client.
Using package manager as npm with npm interface.
Installing packages. This might take a couple minutes.
Installing react-native-scripts...
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.
Installing dependencies using npm...
npm WARN react-native-branch@2.0.0-beta.3 requires a peer of react@>=15.4.0 but none was installed.
npm WARN lottie-react-native@1.1.1 requires a peer of react@>=15.3.1 but none was installed.
Success! Created client at client
Inside that directory, you can run several commands:
npm start
Starts the development server so you can open your app in the Expo
app on your phone.
npm run ios
(Mac only, requires Xcode)
Starts the development server and loads your app in an iOS simulator.
npm run android
(Requires Android build tools)
Starts the development server and loads your app on a connected Android
device or emulator.
npm test
Starts the test runner.
npm run eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!
We suggest that you begin by typing:
cd client
npm start
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:
import React from 'react';
import { StyleSheet, Text, TextInput, KeyboardAvoidingView } from 'react-native';
export default class Login extends React.Component { // (1)
render() {
return (
<KeyboardAvoidingView style={styles.container} behavior="padding"> // (2)
<Text>Enter the name to connect as:</Text> // (3)
<TextInput autoCapitalize="none" // (4)
autoCorrect={false}
autoFocus
keyboardType="default"
maxLength={ 20 }
placeholder="Username"
returnKeyType="done"
enablesReturnKeyAutomatically
style={styles.username}
onSubmitEditing={this.props.onSubmitName}
/>
</KeyboardAvoidingView>
);
}
}
const styles = StyleSheet.create({ // (5)
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center'
},
username: {
alignSelf: 'stretch',
textAlign: 'center'
}
});
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:
import React from 'react';
import Login from './Login';
export default class App extends React.Component { // (1)
constructor(props) {
super(props); // (2)
this.handleSubmitName = this.onSubmitName.bind(this); // (3)
this.state = { // (4)
hasName: false
};
}
onSubmitName(e) { // (5)
const name = e.nativeEvent.text;
this.setState({
name,
hasName: true
});
}
render() { // (6)
return (
<Login onSubmitName={ this.handleSubmitName } />
);
}
}
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.
> npm install --save pusher-js
We then need our component that will make use of this. Write a file ChatClient.js
:
import React from 'react';
import Pusher from 'pusher-js/react-native';
import { StyleSheet, Text, KeyboardAvoidingView } from 'react-native';
import pusherConfig from './pusher.json';
export default class ChatClient extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: []
};
this.pusher = new Pusher(pusherConfig.key, pusherConfig); // (1)
this.chatChannel = this.pusher.subscribe('chat_channel'); // (2)
this.chatChannel.bind('pusher:subscription_succeeded', () => { // (3)
this.chatChannel.bind('join', (data) => { // (4)
this.handleJoin(data.name);
});
this.chatChannel.bind('part', (data) => { // (5)
this.handlePart(data.name);
});
this.chatChannel.bind('message', (data) => { // (6)
this.handleMessage(data.name, data.message);
});
});
this.handleSendMessage = this.onSendMessage.bind(this); // (9)
}
handleJoin(name) { // (4)
const messages = this.state.messages.slice();
messages.push({action: 'join', name: name});
this.setState({
messages: messages
});
}
handlePart(name) { // (5)
const messages = this.state.messages.slice();
messages.push({action: 'part', name: name});
this.setState({
messages: messages
});
}
handleMessage(name, message) { // (6)
const messages = this.state.messages.slice();
messages.push({action: 'message', name: name, message: message});
this.setState({
messages: messages
});
}
componentDidMount() { // (7)
fetch(`${pusherConfig.restServer}/users/${this.props.name}`, {
method: 'PUT'
});
}
componentWillUnmount() { // (8)
fetch(`${pusherConfig.restServer}/users/${this.props.name}`, {
method: 'DELETE'
});
}
onSendMessage(text) { // (9)
const payload = {
message: text
};
fetch(`${pusherConfig.restServer}/users/${this.props.name}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
}
render() { // (10)
const messages = this.state.messages;
return (
<Text>Messages: { messages.length }</Text>
);
}
}
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 usre 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:
{
"appId":"SOME_APP_ID",
"key":"SOME_APP_KEY",
"secret":"SOME_APP_SECRET",
"cluster":"SOME_CLUSTER",
"encrypted":true,
"restServer":"http://192.168.0.15:4000"
}
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:
import React from 'react';
import { StyleSheet, Text, TextInput, FlatList, KeyboardAvoidingView } from 'react-native';
import { Constants } from 'expo';
export default class ChatView extends React.Component {
constructor(props) {
super(props);
this.handleSendMessage = this.onSendMessage.bind(this);
}
onSendMessage(e) { // (1)
this.props.onSendMessage(e.nativeEvent.text);
this.refs.input.clear();
}
render() { // (2)
return (
<KeyboardAvoidingView style={styles.container} behavior="padding">
<FlatList data={ this.props.messages }
renderItem={ this.renderItem }
styles={ styles.messages } />
<TextInput autoFocus
keyboardType="default"
returnKeyType="done"
enablesReturnKeyAutomatically
style={ styles.input }
blurOnSubmit={ false }
onSubmitEditing={ this.handleSendMessage }
ref="input"
/>
</KeyboardAvoidingView>
);
}
renderItem({item}) { // (3)
const action = item.action;
const name = item.name;
if (action == 'join') {
return <Text style={ styles.joinPart }>{ name } has joined</Text>;
} else if (action == 'part') {
return <Text style={ styles.joinPart }>{ name } has left</Text>;
} else if (action == 'message') {
return <Text>{ name }: { item.message }</Text>;
}
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'flex-start',
justifyContent: 'flex-start',
paddingTop: Constants.statusBarHeight
},
messages: {
alignSelf: 'stretch'
},
input: {
alignSelf: 'stretch'
},
joinPart: {
fontStyle: 'italic'
}
});
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:
render() {
const messages = this.state.messages;
return (
<ChatView messages={ messages } onSendMessage={ this.handleSendMessage } />
);
}
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:
render() {
if (this.state.hasName) {
return (
<ChatClient name={ this.state.name } />
);
} else {
return (
<Login onSubmitName={ this.handleSubmitName } />
);
}
}
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.
12 September 2017
by Graham Cox