Building a Persistent React Native Chat App: Part III — React Native App

Technologies: React Native, Redux/React-Redux, Gifted Chat, React Navigation, Socket.io, Sequelize, Postgres

Gabriel Rumbaut
16 min readJul 8, 2018

If this is your first time reading this series, be sure to check out the two previous articles:

It’s Time!

Now that we’ve set up our database and server, it’s finally time to create our React Native mobile app! Exciting!

The first thing we’ll need to do is globally install Facebook’s Create React Native App (CRNA), a tool that creates all of the necessary files for a React Native app and installs connections to Expo — a handy application that will let you test out your app in Xcode, an Android emulator, or on your phone (provided that both your phone and computer are on the same Wi-Fi network).

Run:

npm install -g create-react-native-app

CRNA gives us a few scripts for running our app:

  • npm start will run the app in development mode
  • npm test will run jest
  • npm run ios is like npm start, but it will attempt to open Xcode’s iOS simulator
  • npm run android is also like npm start, but it will attempt to run the app on a connected Android device or emulator
  • npm eject will “eject” from the CRNA build scripts (this is permanent)

We’ll cover running the simulator via Xcode. In order to launch the Xcode simulator using the commands included with CRNA, you’ll need to enable the Xcode command line tools. Open up Xcode, select Xcode from the menu, and then select Preferences… . Next, choose the Locations icon. Under Command Line Tools, select Xcode.

Throughout this tutorial, use npm run ios to test your app. Before running the simulator, be sure to run npm start for your server.

Now we’re ready to create our app. cd out of your server directory, as CRNA will create a new directory. Remember, we want to keep these directories separate to avoid unexpected issues. In your terminal, run:

create-react-native-app chat-app
cd chat-app

Open up the directory in your editor of choice. You’ll note that CRNA has already installed all the necessary dependencies, a test file, and a component called App.js. App.js will be the root of our mobile app. From here, we’ll be exporting our React Navigation <RootStack> component, which we’ll get into in a moment.

First, let’s install our dependencies. We’ll need Socket.io, Gifted Chat, React Navigation, Redux, and React-Redux:

npm install react-native-gifted-chat react-navigation redux react-redux socket.io --save

With all that set up, let’s get building!

Let’s Build a Mobile App!

First, let’s start off by building our Redux store. I prefer to keep all of the Redux reducers separate, so we’ll be using different files. First, create a directory for your store:

mkdir store

Next, let’s create an index.js file as well as files for the different reducers we’ll need. On the back end, we have models for the users (both the logged-in user and everyone else), conversations, and messages. Since the conversations are really only for the backend, we’ll only need Redux reducers for the messages, logged-in user, and the rest of the users:

touch store/index.js store/messages.js store/user.js store/users.js

All of our work with sockets will also be done on the store, so let’s create a file in which we can set up and export our socket connection:

touch store/socket.js

Messages Reducer

Let’s start off by setting up our messages reducer. Recall the events we set up on our server. Users will be able to retrieve all the previous messages in their conversation, as well as send and receive any new messages. We’ll need to include actions and cases in our reducer for those events:

const GOT_MESSAGES = 'GOT_MESSAGES';
const GOT_NEW_MESSAGE = 'GOT_NEW_MESSAGE';
export const gotMessages = messages => ({ type: GOT_MESSAGES, messages });
export const gotNewMessage = message => ({ type: GOT_NEW_MESSAGE, message });
const reducer = (state = [], action) => {
switch (action.type) {
case GOT_MESSAGES:
return action.messages ? action.messages : [];
case GOT_NEW_MESSAGE:
return [action.message, ...state];
default:
return state;
}
};
export default reducer;

User Reducer

Next, let’s set up a reducer for our user. This one will be pretty simple, since for the purposes for our app, we only want someone to be able to login. If we were to include a logout action, we’d just need to reset the user in the store to an empty object:

const GOT_USER = 'GOT_USER';export const gotUser = user => ({ type: GOT_USER, user });const reducer = (state = {}, action) => {
switch (action.type) {
case GOT_USER:
return action.user;
default:
return state;
}
};
export default reducer;

Users Reducer

Our users reducer will be about the same as our messages reducer. Once a user logs in, they’ll be able to see all the currently logged-in users and see users as they login in real time with Socket.io:

const GOT_USERS = 'GOT_USERS';
const GOT_NEW_USER = 'GOT_NEW_USER';
export const gotUsers = users => ({ type: GOT_USERS, users });
export const gotNewUser = user => ({ type: GOT_NEW_USER, user });
const reducer = (state = [], action) => {
switch (action.type) {
case GOT_USERS:
return action.users;
case GOT_NEW_USER:
if(!state.find(user => user.id === action.user.id)) {
return [...state, action.user];
} else {
return state;
}
default:
return state;
}
};
export default reducer;

(Note that for the sake of this tutorial, we’re sending all of an individual user’s information with the users array. In an actual project, you’ll want to hide any confidential information — like passwords — on the back end.)

Setting Up Our Store

Now that we have our reducers in order, we can set up our store. In your store’s index.js file, add:

import { createStore, combineReducers } from 'redux';
import users, { gotUsers, gotNewUser } from './users';
import messages, { gotMessages, gotNewMessage } from './messages';
import user, { gotUser } from './user';
let navigate;const reducers = combineReducers({ users, messages, user });const store = createStore(reducers);export default store;
export * from './users';
export * from './messages';

One item of note here is export *. If you’re not familiar with this, exporting * will allow us to access everything that’s exported from that particular file. Doing this in the store’s index.js file provides an easier way for us to import elsewhere, as we won’t need to target each individual file to get the functions we need.

Adding Sockets

Next, let’s set up our sockets. We’re going to need to import socket.io-client, which allows us to use sockets on the client side:

import io from 'socket.io-client';

In order to set up our socket connection, we’re going to need to pass in a URL. For this tutorial, we’ll be running this app locally, so the URL we pass in will be ‘http://localhost:3000'. (This will need to be changed to your actual server when you’re ready to deploy your app.)

Here’s the code you’ll need for your socket connection:

import io from 'socket.io-client';
const socket = io('http://localhost:3000');
socket.connect();
export default socket;

In your store’s index.js file, import the socket connection:

import socket from './socket';

With that done, we’re ready to add event listeners and emitters. We want our Redux store to update when the events occur, so we’re going to add them directly to our index.js file.

Let’s start by adding our event listeners. Back on our server, we created a few events — ‘priorMessages’, ‘userCreated’, ‘newUser’, and ‘incomingMessage’. Here’s where we’ll listen to those events and handle their specific payloads. On each event, we’ll use store.dispatch to update the Redux store as needed:

socket.on('priorMessages', messages => {
store.dispatch(gotMessages(messages));
});
socket.on('userCreated', response => {
const { user, users } = response;
store.dispatch(gotUser(user));
store.dispatch(gotUsers(users));
navigate('Users');
});
socket.on('newUser', user => {
store.dispatch(gotNewUser(user));
});
socket.on('incomingMessage', message => {
store.dispatch(gotNewMessage(message));
});

Most of these are pretty straightforward, but we should walk through what happens once a user logs in. In the event listener for ‘userCreated’, we’re going to destructure the user object and the users array that the server is sending. On the store, user is an object containing all of the logged-in user’s information, while users is an array of user objects. navigate(‘Users’) is a React Navigation method that will redirect the user to the appropriate screen. We set navigate = navigation.navigate in our login function, which we’ll create in just a moment.

Our event emitters will be handled slightly differently. Since we want events to be emitted on user interaction — such as when a user sends a message — we’ll need to create exported functions that will emit these socket events when invoked. In your store’s index.js file, add:

export const login = (credentials, navigation) => {
socket.emit('newUser', credentials);
navigate = navigation.navigate;
};
export const openChat = users => {
socket.emit('chat', users);
};
export const sendMessage = (text, sender, receiver) => {
socket.emit('message', { text, sender, receiver });
};

We’ll be using each of these functions in the React Native components we’ll be constructing.

Getting Started in React Native

If you’re unfamiliar with React Native, there’s no need to worry. At its core, React Native follows many of the same concepts as React. Just as with React, we’ll be dealing with lifecycle methods, components, and props. Unlike in React, however, we won’t be writing in JSX.

In React Native, we’ll be importing various native components and using those to render the different parts of our application. So, for example, we’ll be using the <Text> component instead of <p> or <h1>, the <View> component instead of <div>, and <TextInput> instead of <input>. Each of these components must be imported from the ‘react-native’ module.

Instead of React Router, we’ll be using React Navigation to handle routing in our app, so let’s start by creating our <RootStack>.

Setting Up React Navigation

Let’s go ahead and delete everything from our App.js file so we can have a fresh start.

As when using React and Redux/React-Redux on the web, we’ll need to first import React, the <Provider> component from React-Redux, and our store:

import React from 'react';
import { Provider } from 'react-redux';
import store from './store';

Next, we’ll need to import createStackNavigator from React Navigation. This is a named export, so be sure to wrap it in braces:

import { createStackNavigator } from 'react-navigation';

In a browser, when a user clicks a link, that particular URL is pushed to the top of the browser’s history stack. We don’t have a history stack here, so we’re going to create one using createStackNavigator. This helps us switch between screens and manage our navigation history.

createStackNavigator takes two arguments: First, it takes an object consisting of the various components we’ll be navigating to. Second, it takes an object with the various options we can configure. createStackNavigator returns a component, which we’ll then use as the entry point in our App.js render function.

Let’s set it up:

const RootStack = createStackNavigator({}, {});

We’ll be filling in the components as we complete them.

With that skeleton of our <RootStack> up, we can now create the App.js component. As is standard with using React-Redux, we’ll need to wrap our root component in React-Redux’s <Provider>:

export default class App extends React.Component {
render() {
return (
<Provider store={ store }>
<RootStack />
</Provider>
);
}
}

So far, your App.js file should look like this:

import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import { createStackNavigator } from 'react-navigation';
export default class App extends React.Component {
render() {
return (
<Provider store={ store }>
<RootStack />
</Provider>
);
}
}
const RootStack = createStackNavigator({}, {});

Login.js

Now we’re ready to build out the components we’ll need! We want our users to be able to login, see the other users they can chat with, and actually chat. Therefore, we’ll need components for login, the user list, and chat. Let’s create those now. First, let’s create a directory for our components:

mkdir components

Next, let’s actually create the files we’ll need:

touch components/Login.js components/Users.js components/Chat.js

Now, let’s create the skeleton for our login component. We’ll need to import React and the Text, View, and StyleSheet components. Since this is going to be a form, we’ll also need to import the TextInput and TouchableOpacity components, as well as the login function from the store to handle the submit:

import React from 'react';
import { View, Text, TextInput, StyleSheet, TouchableOpacity } from 'react-native';
import { login } from '../store';
export default class Login extends React.Component {
constructor() {
super();
}
render() {
return (
);
}
}
const styles = StyleSheet.create({});

A few notes:

  • The lifecycle methods used in React are the same in React Native, as is the basic component structure.
  • StyleSheet.create is a function that takes an object as an argument. This object will contain key-value pairs where the values are style objects with syntax similar to CSS, the key difference being that we’ll be using camel case for the property names (e.g., backgroundColor instead of background-color). We’ll also be using unitless numbers (e.g., borderRadius: 50 instead of border-radius: 50px) and strings. Using style objects like this helps keep our code dry and allows us to pass around styles by ID.

Let’s start by building out our component’s UI. We’re going to need a bit of text to tell the user to enter their name and password, fields for those inputs, and a button for them to submit. We’ll need to wrap these inside a <View>:

<View>
<Text></Text>
<TextInput
/>
<TextInput
/>
<TouchableOpacity
>
<Text></Text>
</TouchableOpacity>
</View>

Our screen will need a style, so let’s add a container style object to StyleSheet.create:

container: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'powderblue',
height: '100%',
width: '100%'
}

Then, let’s pass that object to our <View> inside a style prop:

<View style={ styles.container }>

Between the first set of <Text> tags, add Enter your name and password: without quotes (as if you were writing in JSX). Let’s style as we go, so let’s add the following key-value pair to the StyleSheet.create object:

text: {
fontSize: 20,
fontWeight: 'bold'
}

Then, provide the following prop to the first <Text>:

<Text style={ styles.text }>Enter your name and password:</Text>

Moving on, let’s build out our <TextInput> components and give them some functionality. First, we’ll need to add these fields to our component’s state:

constructor() {
super();
this.state = {
name: '',
password: ''
};
}

We won’t need to pass these values into our inputs, but we will be using the state when we write our handleSubmit function.

Now let’s handle the input changes:

handleChange(type, value) {
this.setState({ [type]: value });
}

And don’t forget to bind these in the constructor so we can maintain the this context when we pass them to our <TextInput> components:

this.handleChange = this.handleChange.bind(this);

Including brackets in this.setState({ [type]: value }) dries out our code a bit, since it allows us to pass in the type argument as the key. (Otherwise, we’d need to write a handleChange function for each input field.)

We can now pass this function to our input fields:

<TextInput>
onChangeText={ value => this.handleChange('name', value}
/>
<TextInput
onChangeText={ value => this.handleChange('password', value}
/>

Let’s add a few more props to our name input field:

<TextInput
onChangeText={ value => this.handleChange('name', value) }
returnKeyType='next'
autoCorrect={ false }
onSubmitEditing={ () => this.passwordInput.focus() }
style={ styles.input }
/>

A few notes:

  • returnKeyType allows us to specify how the return key should look. Here, we want the return key to display next, since we’ll change focus to our password input. This change in focus is handled in the onSubmitEditing prop, which runs once the user hits Next.
  • autoCorrect takes a boolean value and determines whether the mobile device will attempt to correct the user’s input.

Lastly, let’s add an input styles object to our StyleSheet.create:

input: {
height: 40,
width: '90%',
borderWidth: 0.5,
borderColor: 'black',
backgroundColor: '#fff',
color: '#000',
textAlign: 'center',
marginTop: 10
}

Let’s build out our password input as well:

<TextInput
onChangeText={ value => this.handleChange('password', value)}
secureTextEntry
returnKeyType='go'
autoCapitalize='none'
style={ styles.input }
ref={ input => this.passwordInput = input }
/>

There are some slight differences to this input. Here, secureTextEntry hides the text the user’s typing so that the password input isn’t visible. We’re also using a ‘go’ value for returnKeyType and turning off autoCapitalize. The ref prop is what’s enabling us to use this.passwordInput.focus() in the name input field.

Our <TouchableOpacity> will be slightly simpler. First, let’s build our handleSubmit function. Import the login function we created in the store:

import { login } from '../store';

This function takes the user’s credentials (the state) as well as the navigation prop passed in by React Navigation:

handleSubmit() {
login(this.state, this.props.navigation);
}

Lastly, don’t forget to bind this component in our constructor so we can pass it to our <TouchableOpacity> without issue:

this.handleSubmit = this.handleSubmit.bind(this);

Our <TouchableOpacity> will take two props: onPress and style. We’ll also need to wrap it around the text we want to display:

<TouchableOpacity
onPress={ this.handleSubmit }
style={ styles.button }
>
<Text style={ styles.buttonText }>Login</Text>
</TouchableOpacity>

Last, let’s add styles for the button and button text to StyleSheet.create:

button: {
width: '75%',
backgroundColor: 'blue',
borderRadius: 50,
alignItems: 'center',
justifyContent: 'center',
marginTop: 20,
paddingVertical: 15
},
buttonText: {
color: '#fff',
textAlign: 'center',
fontSize: 15,
fontWeight: 'bold',
}

Your Login.js component should look like this:

Now that we have a login component, let’s import it into our App.js file and add it to our stack navigator. Let’s also configure the stack navigator’s initial route to be the login screen. We can also give it a title of ‘Chat!’:

import Login from './components/Login';const RootStack = createStackNavigator({
Login: {
screen: Login
}
}, {
initialRouteName: 'Login',
navigationOptions: {
headerTitle: 'Chat!'
}
});

Users.js

Now we can build out our users list. Let’s start by importing React and the necessary React Native components. We’ll also need to include React-Redux here. We can also set up the component’s basic skeleton:

import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { connect } from 'react-redux';
class Users extends React.Component {
constructor() {
super();
}
render() {
return (
);
}
}
const mapState = state => ({
});
export default connect(mapState)(Users);const styles = StyleSheet.create({});

In this component, we’ll be using Array.map to iterate through the entire list of users and render a simple UI for opening a chat with that user. Whenever the logged-in user clicks on the button belonging to another user, we’ll open the chat between those two users, so let’s start by creating an openChat function and binding it in our constructor:

constructor() {
super();
this.openChat = this.openChat.bind(this);
}
openChat(receivingUser) {
this.props.navigation.navigate('Chat', { receivingUser });
}

In the openChat function, we’re passing the user on the receiving end to React Navigation’s navigate method. Passing in a second argument like this allows us to pass along parameters to the component we’re navigating to. We’ll use this receivingUser object to find or create the chat. To navigate to that screen, all we need is to pass the navigate method a string as the first argument; this string needs to match the name we give in the createStackNavigator function.

With that done, let’s wire up our React-Redux so we’ll actually have data to render:

const mapState = state => ({
users: state.users.filter(user => user.id !== state.user.id)
});

Here, we’re getting the complete list of users from the server. However, since this list includes the currently logged-in user, we’ll want to filter it.

Now let’s get to creating the UI. For each user, we’ll need to render a container with the user’s name and a button allowing us to open a chat:

<View style={ styles.container }>
{
this.props.users.map(user => (
<View key={ user.id } style={ styles.userContainer }>
<Text style={ styles.name }>{ user.name }</Text>
<TouchableOpacity
style={ styles.buttonContainer }
onPress={ () => this.openChat(user) }
>
<Text style={ styles.buttonText }>Chat</Text>
</TouchableOpacity>
</View>
))
}
</View>

Let’s add the following styles to StyleSheet.create:

container: {
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'steelblue',
height: '100%',
width: '100%'
},
userContainer: {
width: '90%',
borderWidth: 1,
borderColor: '#fff',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
marginTop: 20,
marginBottom: 20,
paddingTop: 20,
paddingBottom: 20
},
name: {
textAlign: 'center',
color: '#fff',
fontSize: 25,
fontWeight: 'bold',
marginBottom: 20
},
buttonContainer: {
borderRadius: 50,
backgroundColor: '#fff',
paddingVertical: 15,
width: '75%'
},
buttonText: {
color: 'steelblue',
textAlign: 'center'
}

Your entire Users.js file should be similar to the following:

In App.js, let’s import this component and add to our <RootStack>:

import Login from './components/Login';
import Users from './components/Users';
const RootStack = createStackNavigator({
Login: {
screen: Login
},
Users: {
screen: Users
}
}, {
initialRouteName: 'Login',
navigationOptions: {
headerTitle: 'Chat!'
}
});

Chat.js & Gifted Chat

Now it’s time to build the crux of our app — the chat!

Since we’ll be getting the list of past messages, as well as any new messages, from the Redux store, we’ll need to use React-Redux here. We’ll also need to import GiftedChat (a named export from react-native-gifted-chat) and the openChat and sendMessage functions from the store. Let’s import those and set up the component’s basic skeleton:

import React from 'react';
import { GiftedChat } from 'react-native-gifted-chat';
import { connect } from 'react-redux';
import { openChat, sendMessage } from '../store';
class Chat extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
);
}
}
const mapState = (state, { navigation }) => ({
});
export default connect(mapState)(Chat);

This component will be slightly different from the others we’ve completed. We won’t be writing our own UI . Instead, we’ll be using Gifted Chat to handle the rendering. This component will take a very specific set of objects, which we’ll walk through in just a moment.

First, let’s wire up our React-Redux. We’ll be relying on the store to get the messages and the current user. We can use React Navigation’s navigation.getParam() method to retrieve the receivingUser parameter we’re passing in from the Users component. This method takes the key from the key-value pair we passed in and returns the value.

const mapState = (state, { navigation }) => ({
messages: state.messages,
user: state.user,
receiver: navigation.getParam('receivingUser')
});

Next, let’s write the functions we’ll need to retrieve any past messages associated with the chat and to send messages. Since we want to have any messages appear when the chat loads, we’ll need to hydrate the store when the component mounts:

componentDidMount() {
openChat({ user: this.props.user, receiver: this.props.receiver
});
}
send(message) {
sendMessage(message.text, this.props.user, this.props.receiver);
}

Because we’ll be passing the send function to the <GiftedChat> component and need to maintain our this context, don’t forget to bind send:

this.send = this.send.bind(this);

Next, let’s add Gifted Chat to the render function:

render() {
return (
<GiftedChat
messages={ this.props.messages }
user={{
_id: this.props.user.id
}}
onSend={ message => this.send(message[0])}
/>
);
}

<GiftedChat> requires a few props in order to function properly. The messages prop takes an array of messages. Each message is an object with key-value pairs for the timestamp (createdAt), text, and user. The user part of the message is an object consisting of the sending user’s name and id (key of _id). If you’ve been building along since part I of this series, all this is handled on the back end, so we don’t need to worry about that here.

The user prop takes an object for the sending user.

The onSend prop takes the function we’ll be using to send messages to the receiving user.

With that done, your component should look like this:

Finally, let’s import our Chat component and add it to the stack:

import Login from './components/Login';
import Users from './components/Users';
import Chat from './components/Chat';
const RootStack = createStackNavigator({
Login: {
screen: Login
},
Users: {
screen: Users
},
Chat: {
screen: Chat
}
}, {
initialRouteName: 'Login',
navigationOptions: {
headerTitle: 'Chat!'
}
});

If you’d like to test this app, run npm run ios in your terminal. If you really want to get fancy and test it out on both your mobile device and your simulator, change the socket address to ‘http://[your IP address here]:3000' . This will allow you to actually test out the chat!

And that’s it for our persistent React Native chat app! I hope that you’ve enjoyed this series. If you have any questions or comments, please feel free to drop a line in comments section below.

Thanks for reading!

--

--

Gabriel Rumbaut

I'm a full stack software engineer who's passionate about building new, creative projects.