One of the simplest ways to communicate online is also among the simplest to add to your website, using React and Nexmo's client-side JavaScript tooling. A chat application can provide customer service, facilitate collaboration on a project, or let you catch up with friends. And good news: if you've followed our previous full-stack React and Express tutorial, you already have most of the pieces you need to build one.
Prerequisites
To keep things short, let's assume you have followed the React and Express tutorial.
As in the full-stack example code, the example code for this tutorial will satisfy the latter requirement by using Glitch.
If you use the Nexmo Application you created for your full-stack app, you can generate one or two Conversation IDs and copy those to a text file for use in the next step. If you prefer to create a new application for this project, you can create the conversations from the command line.
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
Adding Chat Rooms to NexmoApp
To keep functionality separate, add a new component to your React application at client/src/Chatroom.js
. For now, you can leave it mostly empty:
import React from 'react';
import styles from './Chatroom.css';
class Chatroom extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
};
export default Chatroom;
You can also add its CSS file, and populate that with whatever styling you like at any stage.
If you open NexmoApp.js
you'll see a couple of references to the Conversation
component. The Chatroom
component will be used similarly, so you can just replace those with references to Chatroom
. They should be in the import
s and the render
function.
Within render
, change the Chatroom
component tag just slightly so that instead of passing invites
, you're passing chats
:
render() {
return (
<div classname="nexmo">
<user onupdate="{this.userUpdated}/">
<chatroom app="{this.state.app}" loggedin="{!!this.state.token}" chats="{this.state.chats}">
</chatroom></user></div>
);
}
The other properties passed to Chatroom
already exist as part of user authentication and logging in, but chats
isn't currently part of the state. For this simple app, hard-code the Conversations you created above into the component's initial state. You can give them any names you like to differentiate them for end users:
constructor(props) {
super(props);
this.state = {
chats: [
{
id: 'CON-123e456c-5ff0-789c-8a11-e4a56a7b8c90',
name: 'nice chat'
},
{
id: 'CON-2c34ecec-f567-8e90-bf1d-23e4567e890a',
name: 'serious business'
}
]
};
this.login = this.login.bind(this);
this.getJWT = this.getJWT.bind(this);
this.userUpdated = this.userUpdated.bind(this);
}
Because this app won't manage conversations or invitations, you can also delete the code in login
to get Conversations. This leaves that function only logging in and storing a reference to the Nexmo application:
login() {
let nexmo = new nexmoClient();
nexmo.createSession(this.state.token).then(app => {
this.setState({
app: app
});
});
}
A Simple Chatroom
You can leave all the User
component code alone. It will continue to do the same thing, creating a new user or offering a list of existing users. Once the user is logged in, they can continue on to chat.
The Chatroom
component will contain two states: choosing a chat room and the chat room itself. Behind the scenes, a chat room is just a Nexmo Conversation, so some of this component code will look similar to what's in the Conversation
component. You can stub out the functions and conditionals needed for both states to get started:
import React from 'react';
import styles from './Chatroom.css';
class Chatroom extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: []
};
this.joinConversation = this.joinConversation.bind(this);
this.onMessage = this.onMessage.bind(this);
this.setInput = this.setInput.bind(this);
this.sendInput = this.sendInput.bind(this);
}
joinConversation(evt) {}
onMessage(sender, message) {}
setInput(evt) {}
sendInput() {}
render() {
if (this.state.conversation) {
} else {
}
}
};
export default Chatroom;
Joining a Chat
Since the user has a finite set of predefined chat rooms to choose from a dropdown will allow them to easily select one. If you only had a single chat, you could do away with this interface entirely. To produce a dropdown for the two chat rooms hard-coded in NexmoApp
, loop over the array to build a set of option
s, then add them as children of a select
:
render() {
if (this.state.conversation) {
} else {
let opts = [<option key="0">-</option>];
this.props.chats.forEach(chat => {
opts.push(<option key="{chat.id}" value="{chat.id}">{chat.name}</option>);
});
return (
<div classname="conversation">
<label>Choose a chat to join:
<select onchange="{evt" ==""> this.joinConversation(evt)}>
{opts}
</select>
</label>
</div>
);
}
}
When the dropdown value changes, joinConversation
gets triggered. The joinConversation
handler will get the chosen conversation by its ID from the Nexmo app and then join it. It also stores a reference to it and assigns it another event handler for incoming messages:
joinConversation(evt) {
let select = evt.target;
this.props.app.getConversation(select.value).then(conv => {
conv.on('text', this.onMessage);
conv.join();
this.setState({
conversation: conv
});
});
}
The onMessage
handler gets triggered whenever there's a new text
event in the active Conversation. It receives information about the Conversation Member who triggered the event, and the event object itself. For a simple chat you can discard most of that information and save only the ID, user display name, and message text. This information can be concatenated onto a list of messages stored in the state:
onMessage(sender, message) {
let newMessages = this.state.messages.concat({
key: message.id,
sender: sender.display_name,
text: message.body.text
});
this.setState({
messages: newMessages
});
}
If you were designing even a simple chat like this for production use, you'd want to plan to move older messages into a different storage object after some time. With any significant amount of traffic, a single array to hold all messages will inevitably cause problems.
Sending Messages
Once the user is logged in and has joined a chat, they'll want to send and receive messages. This means you want to render a UI with, at minimum, an area for viewing messages and an input field for text. The JSX for this fills out the other branch of the main conditional in render
. It iterates over your array of messages and renders anything received since the user joined the chat. Below that, it provides a textarea and button that set newly inputted text and send it, respectively:
render() {
if (this.state.conversation) {
let messagePane = [];
if (this.state.messages.length) {
this.state.messages.forEach(msg => {
messagePane.push(<p key="{msg.key}" classname="message"><b>{msg.sender}:</b>{msg.text}</p>);
});
}
return (
<div classname="conversation">
<div classname="messages">
{messagePane}
</div>
<div classname="input">
<textarea onblur="{evt" ==""> this.setInput(evt)} />
<button onClick={evt => this.sendInput(evt)}>Chat</button>
</div>
</div>
);
} else {
...
}
}
</code></pre>
<p>The events raised by the message input are handled in <code>setInput</code> and <code>sendInput</code>. <code>setInput</code> very simply stores the inputted text in the component state:</p>
<pre><code class="language-javascript"> setInput(evt) {
this.setState({
input: evt.target.value
});
}
</code></pre>
<p>The button handler, <code>sendInput</code>, takes the text stored in the state and passes it to the Conversation using <code>sendText</code>. It then clears the text in the state and in the textarea preceding it:</p>
<pre><code class="language-javascript"> sendInput(evt) {
this.state.conversation.sendText(this.state.input).then(() => {
this.setState({
input: null
});
});
evt.target.previousSibling.value = '';
}
</code></pre>
<h2>Chat Away!</h2>
<p>Though it's missing error handling and pays no attention to performance, now you have a very basic chat application. Stripping away the features of a production app reveals how little you need to provide core chat functionality:</p>
<ol>
<li>A User logged in to a Nexmo Application</li>
<li>A Conversation for the User to join</li>
<li>An event handler for received messages</li>
<li>The <code>sendText</code> function to enable chatting</li>
</ol>
<p>Whether you want to create an old school chat room, a pop-up conversation to help confused customers, or anything else, you can build it starting with these elements. You don't need to handle any sockets or polling. And with React, you don't need to do anything to trigger DOM updates. Now you can turn your attention to your UI and the app's robustness.</p>
</textarea></div></div>
I'm a JavaScript developer and a Developer Educator at Vonage. Over the years I’ve been really excited about templates, Node.js, progressive web apps, and offline-first strategies, but what I’ve always really loved is a useful, well-documented API. My goal is to make your experience using our APIs the very best I can help it be.