Chat is a key piece of most interactive applications. From 1:1 dating apps, group chats, to chatbots, realtime communication is an expectation of any multi-user app. Integrating this functionality is much more seamless if you choose the right framework and infrastructure from the get-go. In this tutorial, we'll show you how to do so - creating a chat window using React, Material-UI, and PubNub.
Our app will allow anyone to connect and chat in realtime on any channel they want. We will create this chat from scratch using the React framework and Material-UI components. The PubNub API is used to handle sending and receiving messages. These three pieces will help us create a modern and fast chat.
Also in this tutorial, we utilize Hooks, a new way of writing React components that reduce redundant code and organizes related pieces. I’ll explain more about why and how we use these new features later in the tutorial. After this tutorial, we will have a chat that allows anyone with a channel name to talk to one another. Channels are represented in the URL and on the page so sharing channels is easy!
Pub/Sub and Retrieving History
PubNub provides a simple and blazingly fast infrastructure for messages to be sent. PubNub is used to connect virtually unlimited amounts of people or systems, in under a quarter second or less, around the world. It has your use cases covered with its numerous SDK’s available, and even chat-focused resource center. In creating this app, we will use Publish/Subscribe for realtime messaging and Storage & Playback to retain messages.
Publishing provides us with a means of sending out messages to those who are listening on specific channels. Learn how to Publish in React.
Subscribing is the way we tell PubNub that we want to receive messages being sent to specific channels. Learn how to Subscribe in React.
Storage & Playback means that someone doesn’t have to be subscribed at the moment to receive messages on a channel. When a user connects we can retrieve the last messages for them to view! Learn how to Store & Playback messages in React.
Getting Started
In this chat example, we only need to utilize one API for all the chat capabilities. You'll need to create a PubNub account or login if you already have an account.
First, get your unique pub/sub keys in the Admin Dashboard, then enable Storage and Playback on the bottom left of your key options page. I set the retention time for my messages to one day, but go ahead and choose whatever time frame works best for you. Be sure to save your changes.
Now that that's set up, we can start setting up our React project.
How to Install React.js and PubNub
In order to install React.js and PubNub, we need to first make sure we have Node.js and npm. Install them at the official Node.js homepage. If you already have them installed, make sure your npm version is above 5.2 by entering npm -v
into your terminal. Now we have our package managers to create our React app and install our PubNub SDK.
Once you install Node.js, run these commands to create your project and install our necessary modules. Wait as React is building you your website! Once that is done, the second line will install PubNub. The third will install our styling framework Material-UI.
npx create-react-app <your-app-name>
npm install --save pubnub
npm install @material-ui/core
We now have all that we need to start coding! If you enter npm start
into your terminal and click on the link it provides once it’s done running, you should see an empty react page! Let’s get to coding!
Why use React Hooks?
Before October of 2018, you had to use class components to store local variables. Hooks brought us the ability to save state inside of functional components and Hooks removed much of the bloat that comes with classes.
Hooks make developing large scale applications easier, its functions help us group together similar code. We organize the logic in our components by what they are doing versus when they need to do it. We forgo the usual lifecycle functions like componentDidMount and componentDidUpdate and instead use useEffect.
useEffect is one of the two main hooks we use, the other being useState. useState is the new setState but works a bit different. The React Hooks documentation goes into detail on a few more, but another great part about Hooks is that we can create our own! This saves time and lines of code by utilizing what we have done already.
I'll show you how to create your own hook, utilize useEffect and useState in the following sections!
Create a Custom React Hook
Let’s start this off by creating our very own hook that simplifies some code for us in the future. Instead of creating onChange functions individually for each input, let’s bundle up what we can for each of them now, in one Hook!
If you look inside your project folder that we created, you can see that we have a few different folders. Navigate into the “src” folder and create a new file there called “useInput.js”. The rules of Hooks state that all hooks have to start with “use”. It also states that Hooks should only be used at the top level so we cannot use them in functions, conditions, or loops. We also cannot call them from regular JS functions, only React function components and custom Hooks! Now that we know the general rules behind them let's create one!
Through this hook, we will use the useState Hook. Import useState from react
at the top of your file and after creating a function named, you guessed it, useInput
.
import { useState } from 'react';
function useInput()
{
//Define our Hook
}
This is where we can get a little funky with our syntax. We can use a destructuring assignment to receive the two objects that useState gives us, using only one line of code. But what is useState giving us? It’s basically returning a getter and setter, a variable that contains the value, and a function to set it! Instead of accessing our state by this.state.xxxxx
, we are able to access it by the name alone.
let [value, setValue] = useState('');
Create a function expression assigned to a new variable we created named onChange. We pass “event” through the function and inside, we set our state value to the event’s target’s value. After let’s return these three variables/functions we’ve created: value, setValue, and onChange.
let onChange = function(event){
setValue(event.target.value);
};
return {
value,
setValue,
onChange
};
Finally export default useInput;
at the end of our file to make it available for our main App to use!
Designing our React Components
Now that we have our Hook completed. Let’s set up our App.js file! We have a few key files to import at the top of our file: React and the two default Hooks we need, our useInput hook we just created, our App.css file, PubNub, and the Material-UI components.
Replace what is in your App.css with the following.
* {
margin: 0;
padding: 0;
}
body {
width: 500px;
margin: 30px auto;
background-color: #fff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
.top {
display: flex;
flex-direction: row;
justify-content: space-between;
}
Let’s make an outline of our chat using our functional component headers. This will help us figure what kind of design and flow we want for our chat. I chose three different components: App, Log, and Message.
App houses the Log, Inputs, and submit button. Log holds a list of Messages, and Message displays the message and who sent it. Make sure to import the required modules at the beginning of your file!
//These are the two hooks we use the most of through this Chat
import React, { useState, useEffect} from 'react';
//has a few css stylings that we need.
import './App.css';
//This is a hook we created to reduce some of the bloat we get with watching inputs for changes.
import useInput from './useInput.js';
//Lets us import PubNub for our chat infrastructure capabailites.
import PubNub from 'pubnub';
//Material UI Components
import {Card, CardActions, CardContent,List, ListItem,Button,Typography,Input} from '@material-ui/core';
// Our main Component, the parent to all the others, the one to rule them all.
function App(){
//Bunch of functions!
//return()
}
//Log functional component that contains the list of messages
function Log(props){
//return()
}
//Our message functional component that formats each message.
function Message(props){
//return()
}
Each of these components includes a return function that allows us to design what each one will look like. We get to say what information we pass down from our parents to our children. Via this design, we only pass information downwards, giving each component what it needs to function.
Setting up App Component: State with React Hooks
Our App is our main React chat component. For this component, there are a few things we need to set up, such as checking the URL for any changes to the channel, setting up our states, then we can make a few useEffect functions to sort what we want App to do, and when all of it happens.
The first action inside of our App is to create a default channel. “Global” is a good one. Then check the URL for a channel. If there isn’t one, then we can leave the default as is, but if there is one there, then we set the default channel to that.
let defaultChannel = "Global";
//Access the parameters provided in the URL
let query = window.location.search.substring(1);
let params = query.split("&");
for(let i = 0; i < params.length;i++){
var pair = params[i].split("=");
//If the user input a channel then the default channel is now set
//If not, we still navigate to the default channel.
if(pair[0] === "channel" && pair[1] !== ""){
defaultChannel = pair[1];
}
}
Let’s define our states with their initial values. Use useState to get getters and setters for our channel, making sure to put our default channel as its initial value. Do the same for our messages array, but initialize it to an empty array.
I also set a generic username for the user, based on the current time. Next set a temporary channel and message variable to the new hook we created. There we go, we have our states set up for our app.
const [channel,setChannel] = useState(defaultChannel);
const [messages,setMessages] = useState([]);
const [username,] = useState(['user', new Date().getTime()].join('-'));
const tempChannel = useInput();
const tempMessage = useInput();
useEffect in React
Next, we get to use the fancy new useEffect everyone’s been talking about. This basically replaces and reorganizes all the old lifecycle methods when we were not using hooks. Each function runs with each rerender unless we specify an array of variables as a second parameter for it to follow. Each time these variables change, the useEffect gets re-run.
REMEMBER: This is a SHALLOW equality check. Numbers and strings will count as different every time you set them as something else, but useEffect only looks at objects pointers, not their attributes.
We can have multiple of these functions, just each of their second parameters needs to be different. Essentially each useEffect is grouped by what it depends on to change, thus actions with similar dependencies run together.
useEffect(()=>{
//Put code we want to run every time these next variables/states change
},[channel, username]);
Setting up PubNub in React
Now that we know how this new Hook works, the next step is to create a new PubNub object! Pull up PubNub to grab those publish and subscribe keys that we generated earlier, and place them in your new object. You can also set a UUID for this connection, whether that be an IP, a username, a generated UUID, or any unique identifier your use case defines. I set it as the username for simplicity’s sake.
const pubnub = new PubNub({
publishKey: "<ENTER-PUB-KEY-HERE>",
subscribeKey: "<ENTER-SUB-KEY-HERE>",
uuid: username
});
After we have our object filled with our connection information, let’s include a Listener for PubNub events! This is useful for detecting new messages, new connections or statuses, and for handling presence events too. Our app doesn’t use presence nor does it require the use of creating a status listener as well, but I at least like to implement status and log some results. What we really need for our app is the ability to receive and handle messages coming in, so let’s define that!
Check if the message text is null or empty, and if it’s not, create a newMessage object. Set the messages array as its current state concatenated with the new message we receive. The arrow function makes sure that we are using the current state of messages and not the initial render’s state.
pubnub.addListener({
status: function(statusEvent) {
if (statusEvent.category === "PNConnectedCategory") {
console.log("Connected to PubNub!")
}
},
message: function(msg) {
if(msg.message.text){
let newMessages = [];
newMessages.push({
uuid:msg.message.uuid,
text: msg.message.text
});
setMessages(messages=>messages.concat(newMessages))
}
}
});
Subscribing to the channel in our state will be our first connection to the PubNub server! If Presence is important to your use case, here is where you enable it. Find out who is in a channel with Presence on the PubNub React SDK.
pubnub.subscribe({
channels: [channel]
});
Incorporating history is a key feature of any chat, so let’s pull a few messages to form a chat log. When we first connect to a channel, use the history function to retrieve the stored messages. Use the response to access the old messages and store them in a temporary array. Since our array should be empty, we can push those old messages into our states empty messages array.
pubnub.history({
channel: channel,
count: 10, // 100 is the default
stringifiedTimeToken: true // false is the default
}, function (status, response){
let newMessages = [];
for (let i = 0; i < response.messages.length;i++){
newMessages.push({
uuid:response.messages[i].entry.uuid ,
text: response.messages[i].entry.text
});
}
setMessages(messages=>messages.concat(newMessages))
});
Another awesome part of useEffect is that we can define behavior that shuts everything down before it runs again! Let’s return a function “cleanup“ and inside, unsubscribe from all channels, and set messages to another empty array.
return function cleanup(){
pubnub.unsubscribeAll();
setMessages([]);
}
Pub/Sub: Publishing
We’ve subscribed to a channel, but we still haven’t published yet. Unlike the PubNub features in the previous useEffect, we want to publish when the user sends a message. Let’s create a function named publishMessage that will publish messages to our channel.
Create the function and check if there is anything in our temporary message there. If there is, create your message object! I included both the message and the username so we know who sent it when we access the messages from any device. Start by creating another PubNub object, exactly the same as the last one. Call publish on it, including our new message and channel as an argument.
After we send the message, clear our temporary message state. This allows the user to send another if they want. Now we don’t have any code calling this function anywhere yet so it won’t fire, but the next function we define will!
function publishMessage(){
if (tempMessage.value) {
let messageObject = {
text: tempMessage.value,
uuid: username
};
const pubnub = new PubNub({
publishKey: "<ENTER-PUB-KEY-HERE>",
subscribeKey: "<ENTER-SUB-KEY-HERE>",
uuid: username
});
pubnub.publish({
message: messageObject,
channel: channel
});
tempMessage.setValue('');
}
}
Creating React Event Handlers
It’s important that we create fluid user interactions with our chat. Let’s create a handler for users to either submit a message or change channels via the ‘Enter’ key. We are going to create one function that I called handleKeyDown, which takes an event object.
function handleKeyDown(event){
//Handling key down event
}
Once we’re inside of this function, our goal is to figure what is triggering this event. Later when we create the inputs we will set IDs for them. Start by checking the event’s target’s id. If it is “messageInput”, do another check if the key pressed was "Enter" or not. If it was, go ahead and call our function publishMessage.
if(event.target.id === "messageInput"){
if (event.key === 'Enter') {
publishMessage();
}
}
Do the same checks to start off this else if statement as the previous, but this time using channelInput
as the ID. Create a constant value that holds our temporary channel, but make sure to trim any leading or trailing whitespace. If we were only calling setChannel here, we wouldn’t need the check if the new and old channels are the same.
Since we also change the current URL to the one we created, we do need the check as there would unneeded duplications. Creating a new URL string that includes the new channel name also allows users to share page links easier. Finally set our temporary channel’s state to an empty string.
else if(event.target.id === "channelInput"){
if (event.key === 'Enter') {
//Navigates to new channels
const newChannel = tempChannel.value.trim()
if(newChannel){
if(channel !== newChannel){
//If the user isnt trying to navigate to the same channel theyre on
setChannel(newChannel);
let newURL = window.location.origin + "?channel=" + newChannel;
window.history.pushState(null, '',newURL);
tempChannel.setValue('');
}
}
//What if there was nothing in newChannel?
}
This is great if the user enters a channel into our input, but what if they don’t? We can either alert them to their mistake, stay at the same channel, or take them to a default channel of our choice. I went with the last option, to take them to “Global”. Do the same check as before, but use "Global" this time and then set the channel as it.
We create a new URL and push it to our page history as before, but without any parameters. The code we included at the beginning of our App will recognize that and use the default channel. Again, set the temp channel to an empty string, making sure to put this code snippet before the last ones ending curly brace.
else{
//If the user didnt put anything into the channel Input
if(channel !== "Global"){
//If the user isnt trying to navigate to the same channel theyre on
setChannel("Global");
let newURL = window.location.origin;
window.history.pushState(null, '',newURL);
tempChannel.setValue('');
}
}
We add the current URL to our browsers back button history in order to give our users the option to navigate to previous channels through that. In order for our chat to actually navigate back and forth between previous channels using the back button, we need to do a few more things.
Navigating Between Previous Channels
Now that we set up all the features for our React chat room, let us add a feature to re-render our page. We will be changing our state, instead of reloading, when a user clicks back or forward between our pages.
Create a function named goBack that checks the URL for a channel and sets either “Global” or the channel found for our channel state. This function won’t run unless we add event listeners to our page!
function goBack() {
//Access the parameters provided in the URL
let query = window.location.search.substring(1);
if(!query){
setChannel("Global")
}else{
let params = query.split("&");
for(let i = 0; i < params.length;i++){
var pair = params[i].split("=");
//If the user input a channel then the default channel is now set
//If not, we still navigate to the default channel.
if(pair[0] === "channel" && pair[1] !== ""){
setChannel(pair[1])
}
}
}
}
We only want to add the listener when the page loads, and to remove it when we leave. That sounds like another use for a useEffect hook! Create another, but pass in an empty array as the second argument. Now, this only runs once per the initial load of our chat. It will not run every rerender.
Create an event listener on our “window”, and return a cleanup function that removes that listener. The event listener will be waiting for “popstate”, which is when the user clicks the back/forward button in their browser. Put the last function we made, “goBack”, after the event name. Now our page won’t reload, it rerenders what it needs when it needs to!
useEffect(() => {
window.addEventListener("popstate",goBack);
return function cleanup(){
window.removeEventListener("popstate",goBack);
}
},[]);
Using JSX to Create a React UI
Now that we have completed all the logic we need in our backend, let us build a simple yet modern front-end! To do this, we return JSX, a JavaScript UI description language. It allows us to use our own variables and objects inside groups called components. The syntax looks something similar to HTML with a templating engine, but it is JSX!
When a variable/state changes, any component that uses it will re-render with the new value. That’s what makes our app feel more responsive, as soon as there is a change it updates. Because of this, using PubNub and React together is a great idea. PubNub is able to deliver messages fast and React keeps up by updating its components!
App Design
Let’s make our design for our App component now. Material-UI provides us with beautiful components that we can use and fill with our own information. Use the following design and we’ll go over what functions are being called in certain areas.
return(
<Card >
<CardContent>
<div className="top">
<Typography variant="h4" inline >
PubNub React Chat
</Typography>
<Input
style={{width:'100px'}}
className="channel"
id="channelInput"
onKeyDown={handleKeyDown}
placeholder ={channel}
onChange = {tempChannel.onChange}
value={tempChannel.value}
/>
</div>
<div >
<Log messages={messages}/>
</div>
</CardContent>
<CardActions>
<Input
placeholder="Enter a message"
fullWidth={true}
id="messageInput"
value={tempMessage.value}
onChange={tempMessage.onChange}
onKeyDown={handleKeyDown}
inputProps={{'aria-label': 'Message Field',}}
autoFocus={true}
/>
<Button
size="small"
color="primary"
onClick={publishMessage}
>
Submit
</Button>
</CardActions>
</Card>
);
It may look like a lot of design here, but it is organizing a few distinct elements.
We first have our title inside of a Typography component. After that in the same div is our channel Input. Inputs include many properties that define the actions it can take. Those include its ID, the function that handles onKeyDown, its placeholder, the onChange function, and its value.
It also has areas to reference its styles as well. After that div, we have our Log, another functional component we have not created yet. That log takes our messages array and will re-render every time that array changes. After our Log, we are able to have another Input and Button. The Input is where the user creates a message. We fill its properties with the respective states and variables that it concerns.
We also set it to auto-focus as well. Set the Button’s onClick to our publish message function to allow the users another way to send their messages. This is the end of our App component and the back-end is completed. Next, we need to create two more small components to display our messages.
Log and Message Design
Our app defines much of how our chat works, but we need two more components to complete it. Both return JSX and organize how our messages are displayed. The first, Log, displays a List of Typography filled ListItems. These ListItems iterate through a map of our messages and output a Message. We create Message with the key of the index in the array, the uuid of the message, and the text of the message as well.
function Log(props) {
return(
<List component="nav">
<ListItem>
<Typography component="div">
{ props.messages.map((item, index)=>(
<Message key={index} uuid={item.uuid} text={item.text}/>
)) }
</Typography>
</ListItem>
</List>
)
};
The Message component represents one single message, a div element, filled with the uuid and the text, separated by a colon. Our App component’s children access the messages by props. They do not get to edit or change, only read and display, what is passed to them.
Now that we have completed defining our components, we finish our app by exporting it at the bottom of our file. The code in index.js will render our App to the webpage! Run npm start
in our project folder and navigate to localhost:3000 in our browser we can see our app up and running!
function Message(props){
return (
<div >
{ props.uuid }: { props.text }
</div>
);
}
export default App;
We have successfully created an app that allows users to chat in channels of their choosing. Check out a live version! The full code repository here as well.
What's Next?
Now that you've got your basic messaging functionality implemented, it's time to add more features! Head over to our Chat Resource Center to explore new tutorials, best practices, and design patterns for taking your chat app to the next level.
Top comments (1)
Nicely written and explained, keep it up!