This is a final and last part of the Building Real time private chatting app using React. In this part, we will finish our application by adding the features listed below,
- If any user is selected from the chat list than we will fetch the conversation between users.
- We will add the real-time chat feature in this application.
- We will add the Logout feature.
A little recap again, To integrate the real-time morality in our application, we connected our React application to Socket Server. Later we implemented the Realtime chat list for the application. On the server-side, we wrote the Socket Server Event to Emit the updated Chat List. And at the end, we Highlighted the selected user from the list of users.
The last thing we did in thethird partwas highlighting a selected user from the chat list to start a new chat. We will start our implementation from where we left in the last part. So let’s start the implementation,
Fetching the conversation between users
As the title reads Fetching the conversation between users, it seems very easy, right! All you have to do is make an HTTP call and you are done. Well, this is exactly what you are going to do but there is a little problem.
ChatList
Component is a separate component and the Conversation
Component will be a separate Component, which we will create in next few minutes. So here you have to pass data between two sibling component.
So first we will create Conversation
Component and then we will display messages and do other things. As always I will divide this section’s explanation into smaller chunks and this way we will speed the development of this application.
Creating Conversation Component
Let’s create Conversation
Component nothing so special about it. Create conversation.js
file inside /home
folder. Right now just write the below code in conversation.js
file,
conversation.js:
import React, { Component } from 'react'; import './Conversation.css'; class Conversation extends Component { render() { return ( <> Conversation Component works! </> ); } } export default Conversation;
Now yourConversation
component is ready to use, So why wait let’s import it into Home
component and see if it works!
Now you can add the <Conversation>
into your Home.js file, and it should show the Conversation Component works!
message. So at this point, your Home.jsfile should look like this.
Home.js:
/* eslint-disable jsx-a11y/anchor-is-valid */import React, { Component } from 'react'; import { withRouter } from "react-router-dom"; import ChatSocketServer from '../../utils/ChatSocketServer'; import ChatHttpServer from '../../utils/ChatHttpServer'; import ChatList from './chat-list/ChatList'; import Conversation from './conversation/Conversation'; import './Home.css'; class Home extends Component { userId = null; state = { isOverlayVisible: true, username: '______', } logout = async () => { try { await ChatHttpServer.removeLS(); ChatSocketServer.logout({ userId: this.userId }); ChatSocketServer.eventEmitter.on('logout-response', (loggedOut) => { this.props.history.push(`/`); }); } catch (error) { console.log(error); alert(' This App is Broken, we are working on it. try after some time.'); throw error; } } setRenderLoadingState = (loadingState) => { this.setState({ isOverlayVisible: loadingState }); } async componentDidMount() { try { this.setRenderLoadingState(true); this.userId = await ChatHttpServer.getUserId(); const response = await ChatHttpServer.userSessionCheck(this.userId); if (response.error) { this.props.history.push(`/`) } else { this.setState({ username: response.username }); ChatHttpServer.setLS('username', response.username); ChatSocketServer.establishSocketConnection(this.userId); } this.setRenderLoadingState(false); } catch (error) { this.setRenderLoadingState(false); this.props.history.push(`/`) } } getChatListComponent() { return this.state.isOverlayVisible ? null : <ChatList userId={this.userId} updateSelectedUser={this.updateSelectedUser}/> } getChatBoxComponent = () => { return this.state.isOverlayVisible ? null : <Conversation /> } render() { return ( <div className="App"> <div className = {`${this.state.isOverlayVisible ? 'overlay': 'visibility-hidden' } `}> <h1>Loading</h1> </div> {/* Application header will go here */} <main role="main" className="container content" > <div className="row chat-content"> <div className="col-3 chat-list-container"> {this.getChatListComponent()} </div> <div className="col-8 message-container"> {this.getChatBoxComponent()} </div> </div> </main> </div> ); } } export default withRouter(Home)
Send data of the selected user from the chat list to the conversation Component
It’s a general thinking that; when you click on any user from the chat list you would like to see the conversation between you and that user. In this section, I will explain how to pass the data from theChatlist
component to theConversation
component.
=>Since you have already written complete chat list code in the last part, hence here you don’t have to write anything.
=>Open theChatList.jsand you will findselectedUser()
method.
ChatList.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ selectedUser = (user) => { this.setState({ selectedUserId: user.id }); this.props.updateSelectedUser(user) }
Explanation:
- The above code is very easy to understand, in the first line we have assigned the user id of the selected user from the chat list to the
selectedUserId
property. - The second line is kinda interesting for us since it would send the data to the other component.
- Here we will use Props and States to send data from ChatList component and receive data in Conversation component respectively.
In the below section we will catch the data sent from theChatlist
component.
Receiving incoming data in Conversation component from ChatList component
Here we are not going to use Redux or any state management libraries since the scope of the app is not that big. Here we will use plain Props and States to transfer data between components.
To receive data from the ChatList component, First, we will have to get the data in Home Component.
Here we will create a state in a Home Component that will hold the currently selected user. This state will be updated by the function, which will be passed as props in the ChatList component.
=>Create a new method called updateSelectedUser()
in Home Component and add the below code in it.
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ updateSelectedUser = (user) => { this.setState({ selectedUser: user }); }
=> Create a new property called selectedUser
in the state object to hold the value of the selected user. Now the State object your Home Class should look like this,
state = { isOverlayVisible: true, username: '______', selectedUser: null }
At this point, the Property of the state in Home Component is set to receive the updated user. Now let’s send the updated user details to Conversation Component using React props.
=> Update the getChatBoxComponent()
method as shown below,
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ getChatBoxComponent = () => { return this.state.isOverlayVisible ? null : <Conversation userId={this.userId} newSelectedUser={this.state.selectedUser}/> }
Fetching the conversation between users from the server
In this section, we will make an HTTP call to fetch the conversation between the user from the server. So to do this, we will use thegetMessages()
method. This method is defined inside theChatHttpServer
class.
So open the Conversation.jsand write down the below code. But first, add below two more properties inside the state object of theConversation
Component class.
Conversation.js:
this.state = { messageLoading: true, conversations: [], selectedUser: null // To hold the currently selected User }
Whenever the Conversation will receive the new user’s data it will call the HTTP service to fetch the conversation between two users. To achieve this we will use two Component Lifecycle methods getDerivedStateFromProps()
andComponentDidUpdate()
respectively.
Now to check the updated user’s data we will we are using getDerivedStateFromProps()
, which update our state with updated data hence the component will rerender itself. Open the Conversation.jsand write down the below code.
Conversation.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ static getDerivedStateFromProps(props, state) { if (state.selectedUser === null || state.selectedUser.id !== props.newSelectedUser.id) { return { selectedUser: props.newSelectedUser }; } return null; }
Once the component will call the render() method, right after theComponentDidUpdate()
method will execute. In this method, we will make an HTTP call to fetch the conversation between two users. Open the Conversation.jsand write down the below code.
Conversation.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ componentDidUpdate(prevProps) { if (prevProps.newSelectedUser === null || (this.props.newSelectedUser.id !== prevProps.newSelectedUser.id)) { this.getMessages(); } }
Explanation:
- The method
ComponentDidUpdate()
will give the previous Props and you will receive the new props in props property of the call. - By using these Two props, we can determine if the logged-in user has selected some other user in the chat list.
- After getting updated user’s data, we will call
getMessages()
method of the class.
Finally, IngetMessages()
method of the class, we will consume theChatHttpServer
class. Once we will receive the new messages,getMessages()
method will update the state and which will render the conversations between two users. Open the Conversation.jsand write down the below code.
Conversation.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ getMessages = async () => { try { const { userId, newSelectedUser} = this.props; const messageResponse = await ChatHttpServer.getMessages(userId,newSelectedUser.id); if (!messageResponse.error) { this.setState({ conversations: messageResponse.messages, }); this.scrollMessageContainer(); } else { alert('Unable to fetch messages'); } this.setState({ messageLoading: false }); } catch (error) { this.setState({ messageLoading: false }); } }
Explanation:
- Here we are calling
getMessages()
, which is defined inside theChatHttpServer
class. - The method
getMessages()
requiresuserId
(logged in user) and thetoUserId
(user selected from the chat list) which nothing but your selected user from the chat list. - Once we receive the response from the server, we will update the state using
conversations
property. - The
scrollMessageContainer
method, as the name suggests it will scroll the content to the bottom.
Let’s write a Nodejs express Route to return the response when we try to fetch the response from/getMessages
route. Open the routes.js and add the below route to it,
routes.js:
this.app.post('/getMessages', routeHandler.getMessagesRouteHandler);
As you can see, we have calledgetMessagesRouteHandler()
method to handle the request and response for this route. So as always, it will send an appropriate response based on the result from the database.
Open the route-handler.js and the below method into it,
route-handler.js:
/* * Real time private chatting app using Angular 2, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */async getMessagesRouteHandler(request, response){ let userId = request.body.userId; let toUserId = request.body.toUserId; if (userId == '') { response.status(CONSTANTS.SERVER_ERROR_HTTP_CODE).json({ error : true, message : CONSTANTS.USERID_NOT_FOUND }); }else{ try { const messagesResponse = await queryHandler.getMessages({ userId:userId, toUserId: toUserId }); response.status(CONSTANTS.SERVER_OK_HTTP_CODE).json({ error : false, messages : messagesResponse }); } catch ( error ){ response.status(CONSTANTS.SERVER_NOT_ALLOWED_HTTP_CODE).json({ error : true, messages : CONSTANTS.USER_NOT_LOGGED_IN }); } } }
Explanation:
- First, we validated the request parameters and based on that server returns the response.
- Then inside the else block, we fetch the messages from MongoDB by calling
getMessages()
method. - If all goes as expected then we return a response to messages.
Now let’s fetch data from database using MongoDB query. Here I expect from you that you are familiar with MongoDB queries.
Open the query-handler.js file and add the below method, this method will basically fetch the messages from the MongoDB.
query-handler.js:
/* * Real time private chatting app using Angular 2, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */getMessages({userId, toUserId}){ const data = { '$or' : [ { '$and': [ { 'toUserId': userId },{ 'fromUserId': toUserId } ] },{ '$and': [ { 'toUserId': toUserId }, { 'fromUserId': userId } ] }, ] }; return new Promise( async (resolve, reject) => { try { const [DB,ObjectID] = await this.Mongodb.onConnect(); DB.collection('messages').find(data).sort({'timestamp':1}).toArray( (err, result) => { DB.close(); if( err ){ reject(err); } resolve(result); }); } catch (error) { reject(error) } }); }
Explanation:
- The first and most important thing here is the MongoDB query. It’s a combination of or and operators, which will basically give the required result.
- And then we return a Promise, which resolves a query result.
And that’s it.
Rendering the conversation as a chat messages
At this point, you have all the messages between two users in conversation
property. The only thing is remaining now, is writing markup so that you can render messages.
=> Now open theconversation.jsand the below markup into the render()
method.
conversation.js:
render() { const { messageLoading, selectedUser } = this.state; return ( <> <div className={`message-overlay ${!messageLoading ? 'visibility-hidden' : ''}`}> <h3> {selectedUser !== null && selectedUser.username ? 'Loading Messages' : ' Select a User to chat.' }</h3> </div> <div className={`message-wrapper ${messageLoading ? 'visibility-hidden' : ''}`}> <div className="message-container"> <div className="opposite-user"> Chatting with {this.props.newSelectedUser !== null ? this.props.newSelectedUser.username : '----'} </div> {this.state.conversations.length > 0 ? this.getMessageUI() : this.getInitiateConversationUI()} </div> <div className="message-typer"> {/* Markup to send messages goes here */} </div> </div> </> ); }
Explanation:
- First, there will be an overlay on the Conversation Component, until you select any user from the chat list.
- Once you will select a user from the chat list, then we will show a message saying that; You are chatting with someone, for example, Chatting with Shashank
- Then based on the conversation state object we will fetch relevant UI (We will see this right after this section).
=> If you have noticed we have used two new methods i.e. this.getMessageUI()
and this.getInitiateConversationUI()
.
=> Thethis.getMessageUI()
will return the conversations in the form messages between two users andthis.getInitiateConversationUI()
will return the placeholder if no user is selected from the chat list.
=> Now open theconversation.jsand add these methods,
conversation.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ alignMessages(toUserId) { const { userId } = this.props; return userId !== toUserId; } getMessageUI () { return ( <ul ref={this.messageContainer} className="message-thread"> { this.state.conversations.map( (conversation, index) => <li className={`${this.alignMessages(conversation.toUserId) ? 'align-right' : ''}`} key={index}> {conversation.message} </li> ) } </ul> ) } getInitiateConversationUI() { if (this.props.newSelectedUser !== null) { return ( <div className="message-thread start-chatting-banner"> <p className="heading"> You haven 't chatted with {this.props.newSelectedUser.username} in a while, <span className="sub-heading"> Say Hi.</span> </p> </div> ) } }
Explanation:
- In
getInitiateConversationUI()
method will return JSX, which will be a placeholder. If the length of the conversation array will be 0 then this method will be called. - In
getMessageUI()
method, we are using.map
will return a list of messages in the form of HTML elements. alignMessage()
method will align the messages to left or right based on the user id of the user.
Now if you look at your screen you should see something similar to the below image,
Sending and Receiving Real-time messages
Finally! The last and the final puzzle of the application, here first we will send a message to the selected user from the chat list. And then, we will write a code to listen for the incoming messages from the server.
Since this section is a little long, hence we will divide this section into two parts. In the first part, we will write code to send the messages. In the second part, we will write code to receive the messages.
Sending real-time messages
So let’s start with the markup open the conversation.jsand write the below code beneath thediv
tag having.message-typer
class.
Here we don’t have too much to understand except keyup Event. In the keyup event, we will send the message to the server when the key code will be13
.
In less technical terms when the user will press enter button we will send the message to the server.
conversation.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ <div className="message-typer"> <form> <textarea className="message form-control" placeholder="Type and hit Enter" onKeyPress={this.sendMessage}> </textarea> </form> </div>
=>We have defined the sendMessage()
which will send the message to the server. So in the conversation.jsfile, add the below method,
conversation.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ sendMessage = (event) => { if (event.key === 'Enter') { const message = event.target.value; const { userId, newSelectedUser } = this.props; if (message === '' || message === undefined || message === null) { alert(`Message can't be empty.`); } else if (userId === '') { this.router.navigate(['/']); } else if (newSelectedUser === undefined) { alert(`Select a user to chat.`); } else { this.sendAndUpdateMessages({ fromUserId: userId, message: (message).trim(), toUserId: newSelectedUser.id, }); event.target.value = ''; } } }
Explanation:
- First, we will check for the validation. If anything goes wrong with that, then we will show the alert.
- After that we inside
else
block, We have called another methodsendAndUpdateMessages()
. - Also, we will push this data into existing
conversation
state array. - Next, we actually send this data to Nodejs server by calling
sendMessage()
method, Which basically emits the socket event. - And at the end, we scroll the scrollbar to the bottom.
Now let’s addsendAndUpdateMessages()
method, inside the sameConversation
class.
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ sendAndUpdateMessages(message) { try { ChatSocketServer.sendMessage(message); this.setState({ conversations : [...this.state.conversations, message] }); this.scrollMessageContainer(); } catch (error) { alert(`Can't send your message`); } }
Explanation:
- We will send this data to Nodejs server by calling
sendMessage()
method, which emits the socket event. - Also, we will push this data into existing
conversation
state array. - And at the end, we scroll the scrollbar to the bottom.
All this work that we just did; is incomplete without writing the nodejs socket event. And also we need to save these messages in MongoDB.
The below socket event will listen to the socket event and based on the user’s id it will send messages to the recipients. Since it’s a socket event that we are emitting from Angular, So open the socket.js and ad the below code,
socket.js:
/* * Real time private chatting app using Angular 2, Nodejs, mongodb and Socket.io * @author Shashank Tiwari * * send the messages to the user */socket.on(`add-message`, async (data) => { if (data.message === '') { this.io.to(socket.id).emit(`add-message-response`,{ error : true, message: CONSTANTS.MESSAGE_NOT_FOUND }); }else if(data.fromUserId === ''){ this.io.to(socket.id).emit(`add-message-response`,{ error : true, message: CONSTANTS.SERVER_ERROR_MESSAGE }); }else if(data.toUserId === ''){ this.io.to(socket.id).emit(`add-message-response`,{ error : true, message: CONSTANTS.SELECT_USER }); }else{ try{ const [toSocketId, messageResult ] = await Promise.all([ queryHandler.getUserInfo({ userId: data.toUserId, socketId: true }), queryHandler.insertMessages(data) ]); this.io.to(toSocketId).emit(`add-message-response`,data); } catch (error) { this.io.to(socket.id).emit(`add-message-response`,{ error : true, message : CONSTANTS.MESSAGE_STORE_ERROR }); } } });
Explanation:
- The first thing would be validation, check if everything is okay. If all goes well then we will go ahead and do the further operation.
- Inside the else block, we are calling two methods
getUserInfo()
andinsertMessages()
; both these methods are defined inside theQueryHandler
class. getUserInfo()
method will fetch socket id of the user who is intended to receive a message and theinsertMessages()
method as the name suggests will store the messages into a database.- And at the end, we will emit an event with the data and this data is nothing but the data received in the
add-message
event.
Receiving Real-time messages
This part is damn so easy to implement. Here we will create a method to listen for the incoming socket events. After receiving the socket event, we will push the new messages in the conversation
state array.
=> So I have createdreceiveMessage()
method inside the ChatSocketServer
class. This method will subscribe for the new incoming message from the socket server.
=> We will call this method from thecomponentDidMount()
method.
=> Once you call this method, it will listen to the incoming messages and automatically push the new messages in the conversation
state array.
So open the conversation.js and add the below inside thecomponentDidMount()
method.
conversation.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ componentDidMount() { ChatSocketServer.receiveMessage(); ChatSocketServer.eventEmitter.on('add-message-response', this.receiveSocketMessages); } componentWillUnmount() { ChatSocketServer.eventEmitter.removeListener('add-message-response', this.receiveSocketMessages); }
Now let’s createreceiveSocketMessages()
method and add the below code to it,
conversation.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ receiveSocketMessages = (socketResponse) => { const { selectedUser } = this.state; if (selectedUser !== null && selectedUser.id === socketResponse.fromUserId) { this.setState({ conversations: [...this.state.conversations, socketResponse] }); this.scrollMessageContainer(); } }
Small tip, in case, if you want to show notifications. For example, you received a new message from Shashank.
You can do that here.
Explanation:
- This method will first check if the messages are from coming from the appropriate user by checking the user Id.
- Once you receive a new message, you will add that message into the
conversation
state property of theConversation
class. - And then again, we will move the scroll bar to bottom as a tradition.
Implementing Logout
Implementing Logout, yeah that the last thing is left to do. So here we will basically work only in HomeComponent
Class. I really don’t need to give an overview of Logout functionality.
So let’s just hop into home.jsadd the below markup in the render()
method, and make sure your header looks like below markup,
home.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ <header className="app-header"> <nav className="navbar navbar-expand-md"> <h4>Hello {this.state.username} </h4> </nav> <ul className="nav justify-content-end"> <li className="nav-item"> <a className="nav-link" href="#" onClick={this.logout}>Logout</a> </li> </ul> </header>
Now open the home.js and the below code. In this below, we will consume the ChatSocketServer
to achieve this functionality.
=> Let’s add a method logout()
in Home component, which makes currently logged in user log out.
home.js:
/* * Real time private chatting app using React, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */ logout = async () => { try { await ChatHttpServer.removeLS(); ChatSocketServer.logout({ userId: this.userId }); ChatSocketServer.eventEmitter.on('logout-response', (loggedOut) => { this.props.history.push(`/`); }); } catch (error) { console.log(error); alert(' This App is Broken, we are working on it. try after some time.'); throw error; } }
Explanation:
- Inside the
logout()
method, we calledremoveLS()
method, which is defined inside theChatService
class. This method will delete the all the data stored in local storage related to this application. - Then After that, we have called the Logout method from
ChatSocketServer
class. This method will send a socket event to the server. This will change the online status to offline. - And once we receive the logout event response we will simply redirect the user to the home page.
Now the last part of the puzzle is socket event that will change the status from online to offline. So open the socket.js and add the below event to it.
socket.js:
/* * Real time private chatting app using Angular 2, Nodejs, mongodb and Socket.io * @author Shashank Tiwari */socket.on('logout', async (data)=>{ try{ const userId = data.userId; await queryHandler.logout(userId); this.io.to(socket.id).emit(`logout-response`,{ error : false, message: CONSTANTS.USER_LOGGED_OUT, userId: userId }); socket.broadcast.emit(`chat-list-response`,{ error : false , userDisconnected : true , userid : userId }); } catch (error) { this.io.to(socket.id).emit(`logout-response`,{ error : true, message: CONSTANTS.SERVER_ERROR_MESSAGE, userId: userId }); } });
Explanation:
- The below line will modify the online status of the user by making a query into MongoDB database.
await queryHandler.logout(userId);
- After doing that this event will emit the information of the logged out user in order to update the chat list.
- And on the front end, Angular will change the online status of the user.
Final thoughts
If you are still with me from the start and you followed each and every step, then pat yourself on your back.
This series was too big, but I thought it was worth writing it. But the good thing is now you know, how to create a private chatting application in React with minimum complexity, for now, that’s the end of this series.
I highly recommend you guys to clone the source code from GitHub and try to relate to each and every point.
If you have any suggestions, question or feature requests, let me know in the below comment box, I would be happy to help. If you like this article, do spread a word about it and share it with others.
Hi, I clones your git repo but the code doesn’t even work..
I get errors about the “toLowerCase()” being undefined when trying to register a user..
I removed these functions and tested and I just get a server error every time I try to sign up a user, do you get the same when you run your code?
Hey Can you post the error? And just to check you have downloaded the code from Github?
Hey there, am having problems still at the level of installation in both folders: a couple of errors show up when I run npm install command so I haven’t been able to test the code. Yes cloned the code from github.
Sample error for Nodejs API: bcrypt@3.0.4 install: `node-pre-gyp install –fallback-to-build`
sample error for React App: Not Found – GET https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.4.0.tgz (‘react-router-dom@4.4.0’ is not in the npm registry). Am I missing something please?
Hi there,
1. For Nodejs API error, you can try below command
npm install --global --production windows-build-tools
If this doesn’t work read these links,
https://github.com/kelektiv/node.bcrypt.js/issues/692
https://stackoverflow.com/questions/51222536/error-while-installing-bycrpt-in-nodejs-whenever-i-run-npm-install-install-sa
Also, try to clear the NPM cache.
2. For React App error, you need to update the NPM libraries and it will just work fine.
Let me know if the above solutions are not working for you.
Both Apps are running now but whenever I try to create an account, it says username already taken no matter what I change it to.
Can you share mongodb schema