Build a Real-time chat app with React Native, Redux-Saga, and Parse Server

Dave Gavigan
The Startup Lab
Published in
5 min readMar 4, 2018

--

Learn how to implement Redux-Sagas with Websockets

Parse Server

If your launching a minimal viable product or proof of concept, Parse Server can be a great option for your backend services, allowing you to focus on the web or mobile client. It even offers websocket support, which it dubs “Live Queries”. We can subscribe to a given parse query and listen for changes to that record to build real-time apps.

This guide assumes a working knowledge of Redux and WebSockets

Our Data Schema

There a couple ways we can go about this in parse server

  1. Subscribe to a whole collection and treat each record as a new entry; This makes the query easy and easier to ID our current user in the chat, but will add a lot of records to the mongoDB collection (parse defaults with MongoDB). Not great if we want multiple chats open at the same time.
  2. Subscribe to a single record and listen to changes in an array. Great to support multiple chat “rooms”, keeps the records down, but can make it a little tricky to ID the current user as user1 or user2

We’ll go with Option 2 because we want to build a chat app used by multiple parties. Create the following columns.

Columns

user1  | user2  | title   | history |
------------------------------------------
User | User | String | Array |
------------------------------------------
Zdsqwd | fFe2dqwd | MyChat | [{}..]

We will use the Gifted Chat project for a real sleek chat UI. You will notice later on that this arrangement in Parse with the array will make it easy to pull in messages to the client.

Configure your collection for live queries

This configuration shows Parse Server running as express middleware. You could also run it as a command line utility.

let api = new ParseServer({  databaseURI: databaseUri || 'mongodb://localhost:27017/dev',  cloud: process.env.CLOUD_CODE_MAIN || __dirname + '/cloud/main.js',  appId: process.env.APP_ID || '{YOURAPPID}',  masterKey: process.env.MASTER_KEY || '{YOURMASTERKEY}',   serverURL: process.env.SERVER_URL || 'http://localhost:1337/parse',    liveQuery: {  classNames: ["Messages"] // List of classes to support for query subscriptions  }});//create an express applet app = express();//define the context root for parse apiapp.use('/parse', api);
//create the http servervar port = process.env.PORT || 1337;var httpServer = require('http').createServer(app);httpServer.listen(port, function() { console.log('parse-server-example running on port ' + port + '.');});// This will enable the Live Query real-time serverParseServer.createLiveQueryServer(httpServer);

Redux-Saga

Sagas help us with control flow over our asynchronous action in redux. You might be using Redux-Thunk in your application, but they both accomplish the same goal. The cool thing about Redux-Saga is that it uses generator functions giving us access to neat features like yield -ing promises to have really nice clean and readable code. Promises are great, but you sometimes can get into a nasty promise chain hell when you need to use multiple in succession.

Redux-Channels

We can listen for Redux actions in our saga to complete some kind of async operation, like calling a REST service for our data. Channels are a mechanism to dispatch actions from third party events outside of our Redux actions, like listening to a websocket connection. The channel will handle actions we emit from these different websocket events and process them as they come in, allowing us to take further action in our redux-store.

Setup our Parse Server queries.

  1. getMessageItems() — fetches the chat rooms for which our user is involved
  2. createSubscription() — creates the Live Query for a given room to listen
function getMessageItems(payload){   let messages = Parse.Object.extend("Messages");   let messageQuery1 = new Parse.Query(messages);   let messageQuery2 = new Parse.Query(messages);   let userPointer = {     __type:"Pointer",    className:"_User",    objectId:payload.user.objectId //current users Id   }  //user could be in either column so we will use a compound query
messageQuery1.equalTo("user1", userPointer);
messageQuery2.equalTo("user2", userPointer);
//the compound query using "or"
let mainQuery = Parse.Query.or(messageQuery1, messageQuery2);
//include the users data so we can use their name in the UI
mainQuery.include("user1");
mainQuery.include("user2")
//return a promise with users chat rooms
return mainQuery.find()
}//accepts chat payload object containing user1 and user2
function createSubscription(payload){
let messageQuery = new Parse.Query("Messages");
let user1Pointer = {
__type:"Pointer",
className:"_User",
objectId:payload.user1.objectId
}
let user2Pointer = {
__type:"Pointer",
className:"_User",
objectId:payload.user2.objectId
}
messageQuery.equalTo("user1", user1Pointer);
messageQuery.equalTo("user2", user2Pointer);
//returns the Parse Live Query (or websocket)
return messageQuery.subscribe()
}

Dispatch an action to start a Parse Live Query

//user clicks on chat, dispatch the action with the chat object
//set this up on something like "mapDispatch" via the connect util
dispatch({type:'INIT_SOCKET_CONNECTION', payload: chatObject});

Configure Sagas with our Channel

//Watcher function loaded into our RootSaga
export function* LiveMessages(){
yield takeLatest(INIT_SOCKET_CONNECTION, initLiveMessages);
}

//Saga function we will set in the background via 'fork'. This will //listen for the "close" action and close our channel.
function* closeSocketConnection(channel){
yield take({type:"MESSAGE_CLOSE_SUBSCRIPTION"});
channel.close();
}
//The Magic Sauce: Saga Event Channel
//Think of this as our way to publish new Redux Actions in a socket
function websocketInitChannel(payload) {
return eventChannel( emitter => {
//init the connection here that we defined earlier
//payload is the userChat oject we clicked on
const subscription = createSubscription(payload);
subscription.on('open', () => {
return emitter({type:"SUBSCRIPTION_OPENED"})
})
subscription.on('update', (data) => {//we're only interested in the new message here, you could also //publish the entire update record, its up to you.

//get the updated conversation
let messages = data.toJSON().conversation;
//get the newest message
let newMessage = messages[messages.length -1]
return emitter({type:"NEW_MESSAGE_RECIEVED", data:newMessage})
});
... and so forth for other events in the connection// unsubscribe function, this gets called when we close the channel
return () => {

//Close the connection
Parse.LiveQuery.close();
return emitter({type:'SUBSCRIPTION_CLOSED'})
}
})//Worker Saga
export function* initLiveMessages(action){

const chan = yield call(websocketInitChannel, action.payload);
//fires our closeSocketConnection saga to run in the background
//and listens for the moment we tell it to close the channel
yield fork(closeSocketConnection, chan);
try {
while (true) {
let msgAction = yield take(chan)
//dispatch the action to the redux store and load new chat
yield put(msgAction);
}
} finally {
console.log('message stream terminated');
}
}
}

TODO: We’ll update this post to include a sample project ASAP!

Parse may not be suited for large scale production, but offers a great way to get a backend out of the box that stays within your control. You can actually tweak your Live Query server to handle even more messages. We could just as easily apply these patterns to other real-time services like firebase and more.

Hope this helps!

--

--

Dave Gavigan
The Startup Lab

Web & Mobile Developer, Founder of https://www.thestartuplab.io . I help people build their products and launch their business