Implementing Chat in JavaScript, Node.js and React Apps

October 31, 2017
Written by
Kevin Thompson
Contributor
Opinions expressed by Twilio contributors are their own

rsJHVkPzN6JInuKhnWzcYNV2eCa8K8HZbz6BypiKPkV2YOXF5wQ3spsJlKUNd3afUK9hP1Btb8-3Hz2dWliQcZz_2I3HXzXVzBv8nOaRO7Ywx7LwWTSiCFdhmYnKDINoo-geh_Wx

This tutorial was built on an earlier version of the Twilio Programmable Chat SDK.

For new development, we suggest this tutorial: Build a Chat App with Twilio Programmable Chat and React.js.

 

If you’re building a chat user interface using JavaScript React, how do you integrate the rest of the backend functionality into your application? In this article, we’ll start by cloning a Git repository with the completed chat interface, then implement Programmable Chat and test sending and receiving messages between multiple users.

Getting Started

We’re going to start with a simple React application that has just a few components for submitting and displaying messages. With git and npm installed, we can clone the repository, install the application’s dependencies, and start the application:

git clone https://github.com/kevinthompson/react-chat-interface
cd react-chat-interface
npm install
npm start

When we start the application, we’ll see a simple chat interface where we can enter a few messages and see them displayed in our message list.

So far we can only have a one-sided conversation. In order to allow multiple people to chat, we need to configure our application to use Twilio’s Programmable Chat service.

Creating Our Token Server

In order to communicate with the Programmable Chat API from our React application, we’re going to need an authenticated access token. Because we need to use our private credentials to generate these tokens, we’ll want to handle access token generation outside of our client interface.

We’ll start by gathering our API credentials and storing them in a .env file so that we can access them from our server.

# ./.env

TWILIO_ACCOUNT_SID=your_account_sid
TWILIO_API_KEY=your_api_key
TWILIO_API_SECRET=your_api_secret
TWILIO_CHAT_SERVICE_SID=your_chat_service_sid

We can find the account SID in our Twilio account console, and our API key and secret can be generated on the API keys page. To get our chat service SID, we’ll need to visit our Programmable Chat Dashboard and create a new chat service. Click the
“Create new Chat Service” button, give it a name, and then click “create”. We don’t need to change any of the settings on the next screen so we can just click save.

Now that we have our credentials stored in our .env file, we can begin creating our server. The server will be a simple Express application that has a single GET route at /token which will request and return a token from Twilio. We’ll need Express and a few other libraries to handle everything our token route requires. Fortunately, npm makes that easy.

npm install --save express dotenv twilio chance

Now we can create a server.js file with the necessary logic. Use dotenv to pull in our environment variables, twilio to create access tokens, and chance to generate random strings.

// ./server.js

require('dotenv').load()

const Twilio = require('twilio')
const Chance = require('chance')
const express = require('express')
const app = express()

const AccessToken = Twilio.jwt.AccessToken
const ChatGrant = AccessToken.ChatGrant
const chance = new Chance()

app.get('/token', function (req, res) {
  const token = new AccessToken(
    process.env.TWILIO_ACCOUNT_SID,
    process.env.TWILIO_API_KEY,
    process.env.TWILIO_API_SECRET,
  )

  token.identity = chance.name()
  token.addGrant(new ChatGrant({
    serviceSid: process.env.TWILIO_CHAT_SERVICE_SID
  }))

  res.send({
    identity: token.identity,
    jwt: token.toJwt()
  })
})

app.listen(3001, function () {
  console.log('Programmable Chat token server listening on port 3001!')
})

When a request is made to our /token route, we create a new access token, assign a random name to the token identity using Chance, then add the chat grant to the token and return a JSON object containing the identity and token. This server will run on port 3001 since our client application is already making use of port 3000.

With our environment variables defined and our server logic in place, we can start the server. Let’s open a new terminal window, then cd into our project directory and run our use node to start our server.

cd react-chat-interface
node server.js

 

Configuring The Proxy

Our starter project was generated using a tool called Create React App, which gives us a modern suite of build tools including Babel and Webpack. But another great feature of this tool is its ability to proxy any request that does not match a static asset to another server. We’ll use this  functionality to simplify making requests to our token server. To enable this, we need to define the proxy attribute in our ./package.json file.

  "proxy": "http://localhost:3001"

After adding this line we’ll need to restart our client application build process. In our terminal instance we press ctrl-C to stop the process, then run npm start to restart it with the new configuration. Now any request to http://localhost:3000/token in our client application will be proxied to our server running on port 3001.

Getting a Token

Our application has a few presentational components that handle displaying our message list and message form, but for the Programmable Chat integration, we’re going to focus on our top level App component.

Inside of our App component, we’ll use jQuery to make the AJAX request to our server, so we’ll first need to add that dependency.

npm install --save jquery

In our App.js file we’ll import our new dependency and define a default value to the username key in our state. Then we’ll create a getToken method for handling communication with our token server.

Our getToken method will instantiate a new Promise, fetch the token, assign the token’s identity to the username key in our state, then resolve with the token or reject with an error message.

// ./src/App.js

import React, { Component } from 'react'
import MessageForm from './MessageForm'
import MessageList from './MessageList'
import $ from 'jquery'
import './App.css'

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      messages: [],
      username: null,
    }
  }

// ...

  getToken = () => {
    return new Promise((resolve, reject) => {
      this.setState({
        messages: [...this.state.messages, { body: `Connecting...` }],
      })

      $.getJSON('/token', (token) => {
        this.setState({ username: token.identity })
        resolve(token)
      }).fail(() => {
        reject(Error("Failed to connect."))
      })
    })
  }

// …

Finally, we’ll invoke this method in our componentDidMount React lifecycle method.

// ./src/App.js
// ...

  componentDidMount = () => {
    this.getToken()
      .catch((error) => {
        this.setState({
          messages: [...this.state.messages, { body: `Error: ${error.message}` }],
        })
      })
  }

// ...

If we reload out client interface now and the server is running, we’ll just see the message “Connecting…”, but if the server is not running our error message will be displayed.

Adding Messages Helper Method

Because we need to update the state of our messages in a few places, let’s make ourselves a little helper method to simplify this syntax.

// ./src/App.js
// ...

  addMessage = (message) => {
    const messageData = { ...message, me: message.author === this.state.username }
    this.setState({
      messages: [...this.state.messages, messageData],
    })
  }

// …

Then we can change the previous calls to setting message state to use this.addMessage({ body: "..." }).

Creating a Chat Client

The next step in implementing Programmable Chat is instantiating a chat client, which is what we needed the token for. We’ll use the twilio-chat library to get access to this functionality.

npm install --save twilio-chat

Since our getToken method returns a promise that resolves with the token, we can chain our next method onto the this.getToken() call. In this createChatClient method we’ll instantiate a new TwilioChat instance, passing it the JSON web token value from our token.

// ./src/App.js
// ...

  componentDidMount = () => {
    this.getToken()
      .then(this.createChatClient)
      .catch((error) => {
        this.addMessage({ body: `Error: ${error.message}` })
      })
  }

// ...

  createChatClient = (token) => {
    return new Promise((resolve, reject) => {
      resolve(new TwilioChat(token.jwt))
    })
  }

// ...

 

Joining a Channel

To join a channel, we’ll follow a similar pattern. We’ll chain a new method onto our series of promises in componentDidMount, then define the method’s logic.

// ./src/App.js
// …

  constructor(props) {
    super(props)
    this.state = {
      messages: [],
      username: null,
      channel: null,
    }
  }

  componentDidMount = () => {
    this.getToken()
      .then(this.createChatClient)
      .then(this.joinGeneralChannel)
      .catch((error) => {
        this.addMessage({ body: `Error: ${error.message}` })
      })
  }

// ...

  joinGeneralChannel = (chatClient) => {
    return new Promise((resolve, reject) => {
      chatClient.getSubscribedChannels().then(() => {
        chatClient.getChannelByUniqueName('general').then((channel) => {
          this.addMessage({ body: 'Joining general channel...' })
          this.setState({ channel })

          channel.join().then(() => {
            this.addMessage({ body: `Joined general channel as ${this.state.username}` })
            window.addEventListener('beforeunload', () => channel.leave())
          }).catch(() => reject(Error('Could not join general channel.')))

          resolve(channel)
        }).catch(() => reject(Error(‘Could not find general channel.’)))
      }).catch(() => reject(Error('Could not get channel list.')))
    })
  }

// ...

Inside of our promise we’re first getting a list of subscribed channels from Twilio. Then we’re attempting to get a specific channel named “general”. If the channel is found, then we add another system message, store the channel in our application’s state, and attempt to join the channel. If we join the channel successfully, we notify the user that they’ve joined the channel with their random username, then bind a beforeunload event to the window that will leave the channel so that users who are no longer in the channel don’t contribute to our channel user limit.

If the general channel is not found, we’ll call reject with an error stating that the channel could not be found.

Creating a Channel

While displaying an error about a missing channel might help us in development, it doesn’t do much for someone trying to use our chat application. Instead rejecting our joinGeneralChannel promise with an error message, let’s call a method that will create the general channel for us.

// ./src/App.js
// …

  joinGeneralChannel = (chatClient) => {
    return new Promise((resolve, reject) => {
      chatClient.getSubscribedChannels().then(() => {
        chatClient.getChannelByUniqueName('general').then((channel) => {
          // ...
        }).catch(() => this.createGeneralChannel(chatClient))
      }).catch(() => reject(Error('Could not get channel list.')))
    })
  }

  createGeneralChannel = (chatClient) => {
    return new Promise((resolve, reject) => {
      this.addMessage({ body: 'Creating general channel...' })
      chatClient
        .createChannel({ uniqueName: 'general', friendlyName: 'General Chat' })
        .then(() => this.joinGeneralChannel(chatClient))
        .catch(() => reject(Error('Could not create general channel.')))
    })
  }

// ...

Now if the general channel doesn’t exist, we call our createGeneralChannel method which uses the chat client to create the channel, then we attempt to join the general channel again.

Displaying Messages from Programmable Chat

With a connection established to our Programmable Chat service and a channel stored in our state, we’re ready to bind methods to events in our channel. We’ll add one more method call to our promise chain that will handle binding all of our channel events.

// ./src/App.js
// …

  componentDidMount = () => {
    this.getToken()
      .then(this.createChatClient)
      .then(this.joinGeneralChannel)
      .then(this.configureChannelEvents)
      .catch((error) => {
        this.addMessage({ body: `Error: ${error.message}` })
      })
  }

// ...

  configureChannelEvents = (channel) => {
    channel.on('messageAdded', ({ author, body }) => {
      this.addMessage({ author, body })
    })

    channel.on('memberJoined', (member) => {
      this.addMessage({ body: `${member.identity} has joined the channel.` })
    })

    channel.on('memberLeft', (member) => {
      this.addMessage({ body: `${member.identity} has left the channel.` })
    })
  }

// ...

When a member of the channel joins, leaves, or adds a new message, our application will add a message to our client interface, but when we add a new message we’re still just updating our local state. In order to complete our Programmable Chat integration, we need to update our handleNewMessage method so that it sends our messages to our Programmable Chat service as well.

// ./src/App.js
//…

  handleNewMessage = (text) => {
    if (this.state.channel) {
      this.state.channel.sendMessage(text)
    }
  }

// ...

With that small change, we now have a fully configured programmable chat service that multiple clients can connect to! To test it, open two browser tabs to http://localhost:3000 and type a few messages in each tab.

Wrapping It Up

We’ve covered just the basics necessary to integrate Twilio’s Programmable Chat into a React application, but you could take it further by allowing users to pick their usernames, join multiple channels, and more!

The full source for the application we built here is available on Github, and you can read more about Twilio’s Programmable Chat in the API documentation.

If you have any questions, comments, or suggestions for ways to improve this implementation, you can reach me at any of these: