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:
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.
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:
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:
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.
> 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:
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:
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.
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:
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:
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:
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:
KeyboardAvoidingView
. This is a special wrapper that understands the keyboard on the device and shifts things around so that they aren’t hidden underneath itWe 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:
this
is usedIf you left your application running then it will automatically reload. If not then restart it and you will see it now look like this:
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
:
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:
join
message on the channel, and it adds a message to our listpart
message on the channel, and it adds a message to our listmessage
message on the channel, and it adds a message to our listThis 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.
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:
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:
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.