For some months now, whilst working on React-native, I stumbled upon Pusher which is a good tool for building applications which have a need for real-time message broadcasting. I'll be showing you a bit of what you could use Pusher to do along with React-Native.
In this tutorial you will be building a simple chat application with Pusher, React-native and Redux as a state container. Before we continue, I’d love to point out that this article assumes a fairly good experience with the use of node.js, react, react-native and es6.
Pusher is a real-time communication platform used to send broadcast messages to listeners subscribed 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:
Under the App Keys section of your Pusher project, note the app_id, key, and secret values.
npm install pusher-js -g
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 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.
The app needs a web 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 with Express.js, a minimalist web framework running on node.js.
You need to have Node 4.0 or higher versions installed on your PC. Then in order to do the following; Start the Node Project, Install Express Install Pusher, run the following commands respectively:
npm init
npm install express -g
npm install pusher
- Create a folder for the project called ChatServer
- Inside it an index.js file
- In index.js, require the necessary libraries and create an express app to run at 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 to it the
app_id
,key
, andsecret
values
...
const pusher = new Pusher({
appId: 'YOUR PUSHER APP_ID HERE',
key: 'YOUR PUSHER KEY HERE',
secret: 'YOUR PUSHER SECRET HERE',
})
- Create an endpoint. The endpoint receives chat messages and sends them to pusher to broadcast them to all listeners on the chat channel.
...
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'));
});
- Our index.js file should look like
var express = require('express');
var Pusher = require('pusher');
var app = express();
app.set('port', (process.env.PORT || 5000));
const pusher = new Pusher({
appId: 'YOUR PUSHER APP_ID HERE',
key: 'YOUR PUSHER KEY HERE',
secret: 'YOUR PUSHER SECRET HERE',
})
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'));
});
Let’s now turn to the mobile app:
Open up a terminal and run the following commands to create a new React Native project:
react-native init PusherChat
cd PusherChat
- Install Dependencies. The app will require the following 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, but you start by building the actions that the application will perform. With Redux you have to create application action types, because when you want to change the state of your app (either a message 'received' or 'sent') you need to tell the application's state manager (known as the reducer) how (Action type) and what (Payload or Data) to use to update the state.
We Start by doing the following:
- 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'
- Create Helper functions
You also need helper functions that encapsulate and return the appropriate action_type when called, so that when you want to perform an action on your store, you dispatch the helper functions and pass their payloads to them:
const getChats = (payload) => {
return { type: GET_ALL_CHATS,
payload,
}
}
const newMessage = (payload) => {
return {
type: RECEIVE_MESSAGE,
payload,
}
}
- Pusher Listener
You also need a function that subscribes to pusher and listens for new messages. For every new message 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.sentAt, JSON.stringify(data), () => {})
}
// function that listens to pusher for new messages and dispatches a new
// message action
export function receiveMessage(dispatch) {
const socket = new Pusher('3c01f41582a45afcd689')
const channel = socket.subscribe('chat_channel')
channel.bind('new-message',
(data) => {
addToStorage(data.chat)
dispatch(newMessage(data.chat))
}
)
}
- Api Message Sender
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 sentAt = moment().format()
const chat = { sender, message, sentAt }
axios.get(`http://localhost:5000/chat/${JSON.stringify(chat)}`)
}
- AsyncStorage Onload Message Fetcher
Finally is a function that gets all the chat messages from the device's AysncStorage
. This is needed when first opening the chat app. It loads all the messages the moment the app is open and as makes them available for the component to render by dispatching the getChats
helper function
export function apiGetChats() {
// get from device async storage and not api
return dispatch => {
return AsyncStorage.getAllKeys((err, keys) => {
AsyncStorage.multiGet(keys, (error, stores) => {
const 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 knows how best to affect 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 RECEIVE_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:
We are adding some middlewares
-> redux-thunk and redux-logger
– (redux-thunk allows you to dispatch functions as actions - redux-logger logs every update made to the store; The previous store state and the current store state.)
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
and this is because of the following:
- It helps us render each chat message in row
- Allows us to build the look and feel of each row
- Gives us scrolling out of the box when needed
- 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 KeyboardSpacer from 'react-native-keyboard-spacer'
import { connect } from 'react-redux'
import moment from 'moment'
import { apiSendChat, receiveMessage, apiGetChats } from './../actions/'
const { width } = Dimensions.get('window')
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 10,
},
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: 'column',
justifyContent: 'space-between',
},
textInput: {
height: 30,
width: (width * 0.85),
color: '#e8e8e8',
borderTopColor: '#e5e5e5',
borderTopWidth: 1,
},
msgAction: {
height: 29,
width: 29,
marginTop: 13,
}
})
const username = 'ponty96'
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 } = this.props
dispatch(apiGetChats())
receiveMessage(dispatch)
}
componentWillReceiveProps(nextProps) {
const { dispatch, Chats } = nextProps
const chats = 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.sender }
</Text>
<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.sender}
</Text>
<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 = (e) => {
if(e.nativeEvent.key === 'Enter') {
const message = this.state.text
const username = this.state.username
apiSendChat(username, message)
}
}
render() {
return (
<View style={styles.container}>
<View style={styles.row}>
<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='Username'/>
</View>
<View style={styles.input}>
<TextInput
style={styles.textInput}
onChangeText={(text) => this.setState({text:text})}
placeholder='Type a message'
onKeyPress={this.sendMessage}/>
</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 is called when the user presses the enter key on their keyboard. This function gets the message and username of the sender from the ConversationScreen
Component State
You need to pass the state to the application components. To do this you will use the react-redux library. This allows you to connect the components to redux and access the application state.
React-Redux provides you with 2 things:
- A 'Provider' component which allows you to pass the store to it as a property.
- 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 in the src/ folder to tie everything together:
import React, { Component} from 'react-native';
import { Provider } from 'react-redux'
import configureStore from './store/configureStore'
import ConversationScreen from './screens/conversation-screen';
const store = configureStore();
export default class PusherChatApp extends Component {
render() {
return (
<Provider store={store}>
<ConversationScreen />
</Provider>
)
}
}```
### App Entry Point
Finally we'll update our application entry points so as to render the components we just built.
To do this, we'll edit our index.ios.js and index.android.js files to
```javascript
import React, {
AppRegistry,
Component,
} from 'react-native';
import PusherChatApp from './src/app'
AppRegistry.registerComponent('PusherChat', () => PusherChatApp);
This is just a basic app and to hack away in React-Native and Redux I'll advice you start from Code Cartoons.
You can find the complete project on GitHub.