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

Share this article

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

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.

Frequently Asked Questions (FAQs) on Building a Stateful Real-Time App with React Native and Pusher

How Can I Integrate Pusher with React Native?

Integrating Pusher with React Native involves a few steps. First, you need to install the Pusher library using npm or yarn. Then, you need to initialize Pusher in your React Native application. You can do this by creating a new instance of Pusher, passing in your app key, cluster, and other optional parameters. Once initialized, you can use Pusher’s methods to trigger events, subscribe to channels, and listen for new events.

What are the Key Differences between Pusher and Other Real-Time Communication Libraries?

Pusher stands out for its simplicity, scalability, and reliability. It provides a straightforward API for triggering and listening to events, and it automatically handles connection and reconnection logic. It also scales automatically to handle any number of connections, and it provides a reliable message delivery guarantee.

How Can I Handle Connection Errors in Pusher?

Pusher provides several events that you can listen to in order to handle connection errors. These include ‘error’, ‘state_change’, and ‘disconnected’. By listening to these events, you can take appropriate action when a connection error occurs, such as retrying the connection or notifying the user.

Can I Use Pusher with Expo?

Yes, you can use Pusher with Expo. However, you need to use the ‘pusher-js’ library instead of the ‘pusher-websocket-react-native’ library, as the latter is not compatible with Expo.

How Can I Test My Pusher Integration?

You can test your Pusher integration by triggering events and listening for them in your application. You can also use Pusher’s Debug Console to view the events being triggered and received.

How Can I Secure My Pusher Channels?

You can secure your Pusher channels by using private or presence channels, which require a server-side authentication endpoint. You can also use SSL encryption for all communication between your application and Pusher.

How Can I Optimize My Pusher Usage for Large Numbers of Users?

You can optimize your Pusher usage for large numbers of users by using presence channels to track the online status of users, and by using batch event triggering to send multiple events at once.

How Can I Use Pusher with Redux?

You can use Pusher with Redux by triggering actions in response to Pusher events. You can also use middleware to handle the interaction between Pusher and Redux.

Can I Use Pusher for Offline-First Applications?

Yes, you can use Pusher for offline-first applications. Pusher provides a ‘disconnected’ event that you can listen to in order to handle disconnections, and it automatically reconnects when the connection is restored.

How Can I Debug Issues with My Pusher Integration?

You can debug issues with your Pusher integration by using Pusher’s Debug Console, which shows all events being triggered and received. You can also use the ‘log’ option when initializing Pusher to log all actions to the console.

Ayomide AregbedeAyomide Aregbede
View Author

JavaScript developer and Elixir enthusiast.

chatchriswReactReact-Projectsreal timeredux
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week