Building a Chatroom using ReactJS and Pusher
By: Hector P.
Tags: JavaScript, Web Development, React, Pusher, Programming

Many of our personal and professional interactions are facilitated by real-time communication tools in the digital age. In 2014, Tim Cook said that Apple handles “about 40 billion iMessage notifications per day” worldwide. Given that every conversation in iMessage is essentially a micro-chatroom, we’re going to explore the inner workings of a chatroom by building one using ReactJS and Pusher.

Could you imagine writing messages and not seeing your chatroom in real-time? That’s what would happen if bi-directional communication via a client and server were not possible. Most solutions use Websockets for bi-directional communication, but it requires manual deployment and configuration. Today, we’re going to use Pusher’s Chatkit API to update the chatroom in real-time.

Step 1: Download and open Pusher’s ReactJS template

This tutorial will be largely guided by the template and instructions listed in Pusher’s repo here. The repo is pre-configured with instructions for the Node.js server that handles the Client and Server-side chatroom requests.

Step 2: Create a Pusher Chatkit instance

Go to Pusher’s dashboard to create a Chatkit instance. Once you’ve created your instance, save your instance locator and secret key values.

Step 3: Set up Node.js server for Chatkit API Server-Side interactions

In Pusher’s third step, the Client-side interactions are limited to users being able to join chatrooms, send messages, and view when other users are typing. The Server-side interactions (creating and managing user accounts) can only happen if we install @pusher/chatkit-server via npm install --save @pusher/chatkit-server and update ./server.js with values from Step 2.

Step 4: Create login flow

See Step 4 instructions here. Create a controlled form by making a component called UsernameForm.js in the ./src/components/ folder. Track the username in the state of UsernameForm.js.

Then, update the parent component, App.js , with a function called onUsernameSubmitted(username) that receives the username from UsernameForm.js and makes a POST request to /users route to create new Chatroom users. The POST request also pessimistically sets the state of App.js currentUser to the username value stored in UsernameForm.js ’s state.

Finally, pass onUsernameSubmitted(username) as a prop to UsernameForm.js and add the prop to an onSubmit listener in the form to bring the loop full-circle.

Step 5: Conditionally render the chatroom component

Create a ChatScreen.js component in the ./src folder that will eventually display the user’s messages. Create a toggle for the state in the App.js component called currentScreen that will default to “Login”. Then, update currentScreen to “ChatScreen” when the onUsernameSubmitted(username) function in Step 4 is called. It will only be called when a username is submitted in the Login form. Update App.js to render the code below:

// App.js component render() {

if (this.state.currentScreen === 'Login') {

return <UsernameForm onSubmit={this.onUsernameSubmitted} />

}

if (this.state.currentScreen === 'ChatScreen') {

return <ChatScreen currentUsername=

{this.state.currentUsername} />

}

}

Step 6: Connect your Chatkit instance

In Step 2, we created a Chatkit instance and now, we are ready to connect to our instance via Pusher’s Client-Side API. Install @pusher/chatkit-client via npm install --save @pusher/chatkit-client and update ./ChatScreen.js with the code below:

// ChatScreen.js component componentDidMount () {

const chatManager = new Chatkit.ChatManager({

instanceLocator: 'YOUR INSTANCE LOCATOR',

userId: this.props.currentUsername,

tokenProvider: new Chatkit.TokenProvider({

url: 'http://localhost:3001/authenticate',

}),

})



chatManager

.connect()

.then(currentUser => {

this.setState({ currentUser })

})

.catch(error => console.error('error', error))

}

Use the instance locator to instantiate, userId (which is your username), and token to create a chatManager object that will allow the user to join chatrooms, send messages, and view when other users are typing.

Note: If you encounter the TOKEN EXPIRY TOO FAR AHEAD error, this means your Node.js server time is ahead of the current time. An easy fix is updating your local machine's time. Click on the time in the upper righthand corner and click "Open Date & Time Preferences" to update your local time. Then, restart your server with npm install.

Step 7: Create chatroom within Pusher Chatkit

In a more robust app, the functionality to create a chatroom via the Client or Server should exist by default, but we are creating a room through the Chatkit dashboard here.

Click on the instance you created in Step 2. Then, click on the Console tab in the upper middle area of the page. Then, click on the Rooms tab slightly to the left and click the Create New Room button. Once the room is created, make note of the ID that appears under the room name. You’ll use the ID in Step 8.

Step 8: Subscribe to new messages

Conceptually, you can’t see a chatroom without “subscribing to” or joining one, so we’re going to update ./ChatScreen.js with the code below:

// ChatScreen.js component state = {

currentUser: {},

currentRoom: {},

messages: []

} componentDidMount () {

const chatManager = new Chatkit.ChatManager({

instanceLocator: 'YOUR INSTANCE LOCATOR',

userId: this.props.currentUsername,

tokenProvider: new Chatkit.TokenProvider({

url: 'http://localhost:3001/authenticate',

}),

}) chatManager

.connect()

.then(currentUser => {

this.setState({ currentUser })

return currentUser.subscribeToRoom({

roomId: "YOUR ROOM ID",

messageLimit: 100,

hooks: {

onMessage: message => {

this.setState({

messages: [...this.state.messages, message],

})

},

},

})

})

.then(currentRoom => {

this.setState({ currentRoom })

})

}

Replace “YOUR ROOM ID” with the Room ID from Step 9. When the user logs in, the ChatScreen component will render, which will initialize the ChatManager object that gives you the ability to perform Client-Side and Server-Side actions. We’re going to add an empty currentRoom object and empty messages array to the state.

The Chatkit API has a subscribeToRoom method that will add the user to the chatroom and subscribe them to all and any actions taking place in the chatroom. subscribeToRoom takes an event handler called onMessage that is called in real-time each time a new message is posted via Chatkit’s webhooks feature. When a message is posted, it is appended to the messages array in the state of ./ChatScreen.js .

Step 9: Rendering messages in the Chatroom

In Step 8, we created a messages array in ./ChatScreen.js , so now, we’ll create a component called MessageList.js in the ./src/components folder that will receive the messages array as a prop. The MessageList.js component will then iterate over the array to render every message in a chatbox. Follow the steps for MessageList here to render messages in your Chatroom.

Step 10: Allowing users to send messages

Similar to Step 4, we can make a copy of the UsernameForm.js component to repurpose it as SendMessageForm.js . The new component should have a state for “text”, so the onChange event can update this.state.text and onSubmit event that calls a new function called sendMessage(text) . The new function will be passed down to SendMessageForm.js as a prop for the onSubmit even of the form.

// SendMessageForm.js component state = {

text: '',

}



onSubmit(e) {

e.preventDefault()

this.props.onSubmit(this.state.text)

this.setState({ text: '' })

}



onChange(e) {

this.setState({ text: e.target.value })

if (this.props.onChange) {

this.props.onChange()

}

} <form onSubmit={this.onSubmit}>

<input

type="text"

placeholder="Type a message here then hit ENTER"

onChange={this.onChange}

value={this.state.text}

/>

</form>

// ChatScreen.js component sendMessage(text) {

this.state.currentUser.sendMessage({

text,

roomId: this.state.currentRoom.id,

})

} render (

<SendMessageForm onSubmit={this.sendMessage} />

)

Remember, the subscribeToRoom method from Step 8 takes an event handler called onMessage that is called in real-time each time a new message is posted, so new messages will appear in the MessageList.js component made in Step 9.

Step 11: Adding real-time typing indicators

Create a TypingIndicator.js component in the ./src/components folder using the instructions in Pusher’s document here. At the end of this step, you’ll wind up having a state for usersWhoAreTyping in ChatScreen.js and the following code:

// ChatScreen.js component state = {

currentUser: {},

currentRoom: {},

messages: [],

usersWhoAreTyping: [],

} sendTypingEvent() {

this.state.currentUser

.isTypingIn({ roomId: this.state.currentRoom.id })

.catch(error => console.error('error', error))

} return(

<TypingIndicator usersWhoAreTyping={this.state.usersWhoAreTyping} />

<SendMessageForm

onSubmit={this.sendMessage}

onChange={this.sendTypingEvent}/>

)

Step 12: Adding who’s online list

Create a WhosOnlineList.js component in the /src/components folder using the instructions here.

Step 13: Adding auto-scrolling for new messages

The MessageList.js component renders all messages sent by users, but I was surprised to see the chatroom did not automatically display the most recent message at the bottom of my screen. I looked further into what it would take to make the MessageList auto-scroll down to the newest message and came up with the following code:

// ChatScreen.js component state = {

currentUser: {},

currentRoom: {},

messages: [],

usersWhoAreTyping: [],

scrolled: false,

} sendMessage = (text) => {

this.state.currentUser.sendMessage({

roomId: this.state.currentRoom.id,

text: text

})



this.setState({

scrolled: false

})

} onScroll = () => {

this.setState({

scrolled: true

})

} // MessageList.js component componentDidUpdate() {

setInterval(this.updateScroll,1000);

} updateScroll = () => {

let element = document.querySelector('.message-list-container')

if (this.props.scrolled === false) {

element.scrollTop = element.scrollHeight;

}

} //update the div for message-list-container with onScroll event

<div className="message-list-container" onScroll={() => this.props.onScroll()}>

Step 14: Cosmetic add-ons

Changing color of user’s posts

I’m a fan of rendering the logged in user’s name differently from other users to make posts easily identifiable. To accomplish this goal, I wrote the ternary statement below in the MessageList.js component.

// MessageList.js component { this.props.currentUser.id === message.senderId

? <font className="special-text">{message.senderId}</font>

: {message.senderId}}

Every time you or another user posts a message, the senderId for the message is set equal to your username. The currentUser.id is set equal to the username of the logged in person. If the ternary is true, the username of the logged in person is wrapped in a className called “special-text”, which styles the username using the CSS in Step 15.

Displaying time next to user post

This functionality is a staple in any chatroom application, but it’s not as easy as it seems to implement. The Chatkit API returns the time of messages in UTC time in a format that’s not as simple as HH:MM. I had to write a function called parseTime to convert UTC to military time and then, convert military time to standard time with the AM/PM designation.

<span className="message-list-time">{this.parseTime(message.createdAt)}</span> parseTime = (createdMessage) => {

let date = new Date(createdMessage);

let hours = date.getHours();

let mins = date.getMinutes();

let merid = " AM" if (hours > 12 && mins < 10) {

hours = hours - 12

mins = "0" + mins

merid = " PM"

return hours + ':' + mins + merid

} else if (hours > 12 && mins > 10) {

hours = hours - 12

merid = " PM"

return hours + ':' + mins + merid

} else if (hours < 12 && mins < 10) {

mins = "0" + mins

return hours + ':' + mins + merid

} else {

return hours + ':' + mins + merid

}

}

Displaying “hit return to submit” when length is greater than 2

Slack has a really cool feature that’s subtle in design, but lets users with minimal experience know how to communicate in its app when they type a specific number of characters.

// SendMessageForm.js component {

this.state.text.length > 2 ?

<div className="message-form-return">hit return to send</div> :

""

}

Step 15: Style the chatroom components

The standard for chatroom dictates a navigation menu on the left panel with information about the users on that same left panel. To the right of the panel, the chatroom should render all chat messages and have a text field at the bottom to submit messages. See my styled sheet to update the styling of your chatroom app here.