Mobile
Article

Build a Stateful Real-Time App with React Native and Pusher

By Ayomide Aregbede

Users now expect apps to update and react to their actions in real-time. Thankfully there are a lot of language varieties and libraries now available to help you create these highly dynamic apps. In this tutorial you will learn how to build a real-time chat application with Pusher, React-native and Redux to manage the state of the app.

You can find the complete project on GitHub.

Install Dependencies

Pusher

Pusher is a realtime communication platform used to broadcast messages to listeners via their subscription to a channel. Listeners subscribe to a channel and the messages are broadcast to the channel and all the listeners receive the messages.

You will first need to create an account and then install the Pusher npm module with the following command:

npm init
npm install pusher -g
npm install pusher-js -g

Under the App Keys section of your Pusher project, note the app_id, key, and secret values.

React Native

React Native is a framework for building rich, fast and native mobile apps with the same principles used for building web apps with React.js. React (for me) presents a better way to build UIs and is worth checking out for better understanding of this tutorial and to make your front-end life a lot easier. If you have not used React Native before, SitePoint has a lot of tutorials, including a Quick Tip to get you started.

Redux

Redux is a simple state container (the simplest I’ve used so far) that helps keep state in React.js (and React Native) applications using unidirectional flow of state to your UI components and back from your UI component to the Redux state tree. For more details, watch this awesome video tutorials by the man who created Redux. You will learn a lot of functional programing principles in Javascript and it will make you see Javascript in a different light.

App Backend

First the app needs a backend to send chat messages to, and to serve as the point from where chat messages are broadcast to all listeners. You will build this backend with Express.js, a minimalist web framework running on node.js.

Install Express with the following command:

npm install express -g

Create a folder for the project called ChatServer and inside it an index.js file.

In index.js, require the necessary libraries and create an express app running on port 5000.

var express = require('express');
var Pusher = require('pusher');
var app = express();

app.set('port', (process.env.PORT || 5000));

Create your own instance of the Pusher library by passing it the app_id, key, and secret values:

...

const pusher = new Pusher({
   appId: 'YOUR PUSHER APP_ID HERE',
   key:    'YOUR PUSHER KEY HERE',
   secret: 'YOUR PUSHER SECRET HERE'
})

Create an endpoint that receives chat messages and send them to pusher to make a broadcast action to all listeners on the chat channel. You also need to setup a listener for connections on the set port.

...

app.get('/chat/:chat', function(req,res){
  const chat_data = JSON.parse(req.params.chat);
  pusher.trigger('chat_channel', 'new-message', {chat:chat_data});
});

app.listen(app.get('port'), function() {
  console.log('Node app is running on port', app.get('port'));
});

Mobile App

Now to the mobile app, move up a level and run the following command to create a new React Native project:

react-native init PusherChat
cd PusherChat

The app needs some other dependencies:

  • Axios – For Promises and async requests to the backend.
  • AsyncStorage – For storing chat messages locally.
  • Moment – For setting the time each chat message is sent and arrange messages based on this time.
  • Pusher-js – For connecting to pusher.
  • Redux – The state container
  • Redux-thunk – A simple middleware that helps with action dispatching.
  • React-redux – React bindings for Redux.

You should have already installed pusher-js earlier, and AsyncStorage is part of React native. Install the rest by running:

npm install --save redux redux-thunk moment axios react-redux

Now you are ready to build the chat app, starting by building the actions that the application will perform.

With Redux you have to create application action types, because when you dispatch actions to the reducers (state managers), you send the action to perform (action type) and any data needed to perform the action (payload). For this app the actions are to send a chat, get all chats, and receive a message

Create a new file in src/actions/index.js and add the following:

import axios from 'axios'
import { AsyncStorage } from 'react-native'
import moment from 'moment'
import Pusher from 'pusher-js/react-native';

export const SEND_CHAT = "SEND_CHAT";
export const GET_ALL_CHATS = "GET_ALL_CHATS";
export const RECEIVE_MESSAGE = " RECEIVE_MESSAGE";

You also need helper functions that encapsulate and return the appropriate action_type when called, so that when you want to send a chat you dispatch the sendChat function and its payload:

const sendChat = (payload) => {
    return {
        type: SEND_CHAT,
        payload: payload
    };
};

const getChats = (payload) => {
    return {
        type: GET_ALL_CHATS,
        payload: payload
    };
};

const newMessage = (payload) => {
    return {
        type: RECEIVE_MESSAGE,
        payload: payload
    };
};

You also need a function that subscribes to pusher and listens for new messages. For every new messages this function receives, add it to the device AsyncStorage and dispatch a new message action so that the application state is updated.

// function for adding messages to AsyncStorage
const addToStorage = (data) => {
    AsyncStorage.setItem(data.convo_id+data.sent_at, JSON.stringify(data), () => {})
}


// function that listens to pusher for new messages and dispatches a new
// message action
export function newMesage(dispatch){
    const socket = new Pusher("3c01f41582a45afcd689");
    const channel = socket.subscribe('chat_channel');
    channel.bind('new-message',
        (data) => {
            addToStorage(data.chat);
            dispatch(newMessage(data.chat))
        }
    );
}

You also have a function for sending chat messages. This function expects two parameters, the sender and message. In an ideal chat app you should know the sender via the device or login, but for this input the sender:

export function apiSendChat(sender,message){
    const sent_at = moment().format();
    const chat = {sender:sender,message:message, sent_at:sent_at};
    return dispatch => {
        return  axios.get(`http://localhost:5000/chat/${JSON.stringify(chat)}`).then(response =>{
        }).catch(err =>{
            console.log("error", err);
        });
    };
};

Finally is a function that gets all the chat messages from the device AysncStorage. This is needed when first opening the chat app, loading all the messages from the device storage and starting to listen for new messages.

export function apiGetChats(){
    //get from device async storage and not api

    return dispatch => {
        dispatch(isFetching());
        return AsyncStorage.getAllKeys((err, keys) => {
            AsyncStorage.multiGet(keys, (err, stores) => {
                let chats = [];
                stores.map((result, i, store) => {
                    // get at each store's key/value so you can work with it
                    chats.push(JSON.parse(store[i][1]))
                });
                dispatch(getChats(chats))
            });
        });
    };
}

The next step is to create the reducer. The easiest way to understand what the reducer does is to think of it as a bank cashier that performs actions on your bank account based on whatever slip (Action Type) you present to them. If you present them a withdrawal slip (Action Type) with a set amount (payload) to withdraw (action), they remove the amount (payload) from your bank account (state). You can also add money (action + payload) with a deposit slip (Action Type) to your account (state).

In summary, the reducer is a function that affects the application state based on the action dispatched and the action contains its type and payload. Based on the action type the reducer affects the state of the application.

Create a new file called src/reducers/index.js and add the following:

import { combineReducers } from 'redux';
import { SEND_CHAT, GET_ALL_CHATS, RECEIVE_MESSAGE} from './../actions'

// THE REDUCER

const Chats = (state = {chats:[]}, actions) => {
    switch(actions.type){
       case GET_ALL_CHATS:
            return Object.assign({}, state, {
                process_status:"completed",
                chats:state.chats.concat(actions.payload)
            });

        case SEND_CHAT:
        case NEW_MESSAGE:
            return Object.assign({}, state, {
                process_status:"completed",
                chats:[...state.chats,actions.payload]
            });

        default:
            return state;
    }
};

const rootReducer = combineReducers({
    Chats
})

export default rootReducer;

Next create the store. Continuing the bank cashier analogy, the store is like the warehouse where all bank accounts (states) are stored. For now you have one state, Chats, and have access to it whenever you need it.

Create a new src/store/configureStore.js file and add the following:

import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import rootReducer from '../reducers'

const createStoreWithMiddleware = applyMiddleware(
    thunkMiddleware,
    createLogger()
)(createStore)

export default function configureStore(initialState) {
    const store = createStoreWithMiddleware(rootReducer, initialState)
    return store
}

Now let’s create the main chat component that renders all the chat messages and allows a user to send a chat message by inputting their message. This component uses the React Native ListView.

Create a new src/screens/conversationscreen.js file and add the following:

import React, { Component, View, Text, StyleSheet, Image, ListView, TextInput, Dimensions} from 'react-native';
import Button from './../components/button/button';
import { Actions } from 'react-native-router-flux';
import KeyboardSpacer from 'react-native-keyboard-spacer';
import { connect } from 'react-redux';
import moment from 'moment';
import { apiSendChat, newMesage } from './../actions/';

const { width, height } = Dimensions.get('window');

const styles = StyleSheet.create({
    container: {
        flex: 1
    },
    main_text: {
        fontSize: 16,
        textAlign: "center",
        alignSelf: "center",
        color: "#42C0FB",
        marginLeft: 5
    },
    row: {
        flexDirection: "row",
        borderBottomWidth: 1,
        borderBottomColor: "#42C0FB",
        marginBottom: 10,
        padding:5
    },
    back_img: {
        marginTop: 8,
        marginLeft: 8,
        height: 20,
        width: 20
    },
    innerRow: {
        flexDirection: "row",
        justifyContent: "space-between"
    },
    back_btn: {},
    dp: {
        height: 35,
        width: 35,
        borderRadius: 17.5,
        marginLeft:5,
        marginRight:5
    },
    messageBlock: {
        flexDirection: "column",
        borderWidth: 1,
        borderColor: "#42C0FB",
        padding: 5,
        marginLeft: 5,
        marginRight: 5,
        justifyContent: "center",
        alignSelf: "flex-start",
        borderRadius: 6,
        marginBottom: 5
    },
    messageBlockRight: {
        flexDirection: "column",
        backgroundColor: "#fff",
        padding: 5,
        marginLeft: 5,
        marginRight: 5,
        justifyContent: "flex-end",
        alignSelf: "flex-end",
        borderRadius: 6,
        marginBottom: 5
    },
    text: {
        color: "#5c5c5c",
        alignSelf: "flex-start"
    },
    time: {
        alignSelf: "flex-start",
        color: "#5c5c5c",
        marginTop:5
    },
    timeRight: {
        alignSelf: "flex-end",
        color: "#42C0FB",
        marginTop:5
    },
    textRight: {
        color: "#42C0FB",
        alignSelf: "flex-end",
        textAlign: "right"
    },
    input:{
        borderTopColor:"#e5e5e5",
        borderTopWidth:1,
        padding:10,
        flexDirection:"row",
        justifyContent:"space-between"
    },
    textInput:{
        height:30,
        width:(width * 0.85),
        color:"#e8e8e8",
    },
    msgAction:{
        height:29,
        width:29,
        marginTop:13
    }
});
const username = 'DUMMY_USER';

function mapStateToProps(state) {
    return {
        Chats: state.Chats,
        dispatch: state.dispatch
    }
}

class ConversationScreen extends Component {

    constructor(props) {
        super(props);
        const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 != r2});
        this.state = {
            conversation: ds,
            text:"",
            username
        }
    }

    componentDidMount(){
        const {dispatch, Chats} = this.props;
        const chats = Chats;
        chats.sort((a,b)=>{
                return moment(a.sent_at).valueOf() - moment(b.sent_at).valueOf();
            });
            this.setState({
                conversation: this.state.conversation.cloneWithRows(chats)
            })
    }
    componentWillReceiveProps(nextProps) {
        const {dispatch, Chats} = this.props;
        const chats = Chats;
        chats.sort((a,b)=>{
                return moment(a.sent_at).valueOf() - moment(b.sent_at).valueOf();
            });
            this.setState({
                conversation: this.state.conversation.cloneWithRows(chats)
            })

    }

    renderSenderUserBlock(data){
        return (
            <View style={styles.messageBlockRight}>
                <Text style={styles.textRight}>
                    {data.message}
                </Text>
                <Text style={styles.timeRight}>{moment(data.time).calendar()}</Text>
            </View>
        )
    }
    renderReceiverUserBlock(data){
        return (
            <View style={styles.messageBlock}>
                <Text style={styles.text}>
                    {data.message}
                </Text>
                <Text style={styles.time}>{moment(data.time).calendar()}</Text>
            </View>
        )
    }
    renderRow = (rowData) => {
        return (
            <View>
                {rowData.sender == username ? this.renderSenderUserBlock(rowData) : this.renderReceiverUserBlock(rowData)}
            </View>
        )
    }

    sendMessage = () => {

        const message = this.state.text;
        const username =  this.state.username;

        const {dispatch, Chats} = this.props;
        dispatch(apiSendChat(username,message))

    }

    render() {
        return (
            <View style={styles.container}>
                <View style={styles.row}>
                    <Button
                        style={styles.back_btn}
                        onPress={() => Actions.pop()}>
                        <Image source={require('./../assets/back_chevron.png')} style={styles.back_img}/>
                    </Button>
                    <View style={styles.innerRow}>
                        <Image source={{uri:"https://avatars3.githubusercontent.com/u/11190968?v=3&s=460"}} style={styles.dp}/>
                        <Text style={styles.main_text}>GROUP CHAT</Text>
                    </View>
                </View>

                <ListView
                    renderRow={this.renderRow}
                    dataSource={this.state.conversation}/>

                <View style={styles.input}>

                    <TextInput
                        style={styles.textInput}
                        onChangeText={(text) => this.setState({username:text})}
                        placeholder="Send has?"/>
                    <TextInput
                        style={styles.textInput}
                        onChangeText={(text) => this.setState({text:text})}
                        placeholder="Type a message"/>
                    <Button
                        onPress={this.sendMessage}>
                        <Image source={require('./../assets/phone.png')} style={styles.msgAction}/>
                    </Button>
                </View>
                <KeyboardSpacer/>
            </View>
        )
    }
}

export default connect(mapStateToProps)(ConversationScreen)

React Native gives you a lifecycle function, componentWillReceiveProps(nextProps) called whenever the component is about to receive new properties (props) and it’s in this function you update the state of the component with chat messages.

The renderSenderUserBlock function renders a chat message as sent by the user and the renderReceiverUserBlock function renders a chat message as received by the user.

The sendMessage function gets the message from the state that the user intends to send, the username of the recipient and dispatches an action to send the chat message.

The renderRow function passed to the Listview component contains properties and renders the data of each row in the Listview.

You need to pass state to the application components and will use the React-redux library to do that. This allows you to connect the components to redux and access to the application state.

React-Redux provides you with 2 things:

  1. A ‘Provider’ component which allows you to pass the store to it as a property.
  2. A ‘connect’ function which allows the component to connect to redux. It passes the redux state which the component connects to as properties for the Component.

Finally create app.js to tie everything together:

import React, { Component, StyleSheet, Dimensions} from 'react-native';
import { Provider } from 'react-redux'
import configureStore from './store/configureStore'

const store = configureStore();

import ConversationScreen from './screens/conversation-screen';

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: "#fff",
    },
    tabBarStyle: {
        flex: 1,
        flexDirection: "row",
        backgroundColor: "#95a5a6",
        padding: 0,
        height: 45
    },
    sceneStyle: {
        flex: 1,
        backgroundColor: "#fff",
        flexDirection: "column",
        paddingTop:20
    }
})


export default class PusherChatApp extends Component {
    render() {
        return (
            <Provider store={store}>
            <ConversationScreen />
                </Provider>
        )

    }
}

And reference app.js in index.android.js and index.ios.js, replacing any current contents:

import React, {
   AppRegistry,
   Component,
   StyleSheet,
   Text,
   View
 } from 'react-native';

 import PusherChatApp from './src/app'

 AppRegistry.registerComponent('PusherChat', () => PusherChatApp);

Talk to Me

And that’s it, a scalable and performant real-time app that you can easily add to and enhance for your needs. If you have any questions or comments then please let me know below.

  • http://www.sinkernet.com Ayorinde

    Thanks for sharing

  • Dmitriy WebMaker

    hi, thx for this lesson.
    Can u recomend some libs, if I wish realize my own push server?

  • http://jsapp.me Herve

    Great article but the github repo does not work. I get multiples errors when I try to test it. Too bad I would really love to give it a try.

    Hervé

    • Chris Ward

      Hmm, I tested the repo @disqus_8F7ycBD8Gp:disqus are you getting any errors in particular?

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Mobile, once a week, for free.