DEV Community

loading...
Cover image for Build a chat app with Twilio and KendoReact
Twilio

Build a chat app with Twilio and KendoReact

philnash profile image Phil Nash Originally published at twilio.com on 惻11 min read

Twilio Programmable Chat provides an SDK and robust back-end for real time chat applications, but it's missing a front-end. If you need a chat UI, as well as a whole bunch of other useful components, then KendoReact might be what you're looking for.

Kendo UI provides well designed and tested components that you can use within your React, Angular, Vue and jQuery applications. In this post we will build a Twilio Chat application with React and the KendoReact conversational UI components.

What you'll need

If you want to build along with this tutorial, then you'll need a few things:

If you want to skip ahead, you can check out the code for this application in this GitHub repo.

Let's get started

We're going to use the React and Express starter application that I built in this post as the basis for this app. This app gives us an easy way to run a Node.js server and React front-end with one command and comes with endpoints ready to create Access Tokens for Twilio Programmable Chat. Download or clone the application, change into the directory, and install the dependencies:

git clone -b twilio https://github.com/philnash/react-express-starter.git twilio-chat-kendo
cd twilio-chat-kendo
npm install

Copy the .env.example file to .env then fill in the blanks with your Twilio account SID, the chat service, and API keys you generated earlier.

cp .env.example .env

Run the application to make sure everything is working so far. On the command line run:

npm run dev

You will see an application that looks like this open in your browser at localhost:3000.

We have our Twilio Chat application ready and our React app set up. Let's get building.

Preparing to chat

There's a bit of work we need to do before we start on the chat integration. We need to install some dependencies, remove the example app, and add a bit of style. Let's start with those dependencies.

We'll need the twilio-chat module to connect with Twilio Chat and then a few KendoReact modules that will provide the components we're going to use:

npm install twilio-chat @progress/kendo-react-conversational-ui @progress/kendo-react-inputs @progress/kendo-react-buttons @progress/kendo-react-intl @progress/kendo-theme-material

Next, strip src/App.js back to the basics, including the CSS for the KendoReact Material theme:

import React, { Component } from 'react';
import '@progress/kendo-theme-material/dist/all.css';

class App extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    return <p>Hello world</p>;
  }
}

export default App;

To give the application a bit more style and layout (without too much effort) add the Bootstrap CSS to the <head> of public/index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- rest of the head -->
    <title>React App</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
          integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
      crossorigin="anonymous"
    />
  </head>

With that done it's time to build our first component.

Building a login form

For users to join our chat we need them to log in and choose a username. If you are building this into an existing application, you probably already have users and a login system. For this post we're just going to fake it by presenting a login form that asks for a username.

Create a new file, src/Login.js, and open it up. We'll make this a functional component as the login form itself doesn't need to store any state. Start with the following boilerplate:

import React from 'react';

const Login = props => {
  return;
};
export default Login;

To make our Login form fit in with our conversational UI, we'll use KendoReact components. At the top import the Button and Input components:

import React from 'react';
import { Button } from '@progress/kendo-react-buttons';
import { Input } from '@progress/kendo-react-inputs';

Modify the Login function to return the following JSX:

const Login = props => {
  return (
    <form className="k-form" onSubmit={props.handleLogin}>
      <fieldset>
        <legend>Log in</legend>
        <div className="mb-3">
          <Input
            name="username"
            label="Username"
            required={true}
            style={{ width: '100%' }}
            value={props.username}
            onChange={props.handleUsernameChange}
          />
        </div>
        <div>
          <Button type="submit" primary={true}>
            Sign in
          </Button>
        </div>
      </fieldset>
    </form>
  );
};

That's quite the chunk of JSX, so let's break it down. The whole thing is a <form> containing a <fieldset> and <legend>. Then inside there is an <Input> component and a <Button> component. These are the KendoReact components that we imported. They act like regular <input> and <button> elements but fit in with the KendoReact style.

The JSX also includes some properties we need to provide the component with; a username and two functions to handle events. We'll add these to the <App> component so we can pass them in as properties.

Open up src/App.js and start by importing the new <Login> component.

import React, { Component } from 'react';
import '@progress/kendo-theme-material/dist/all.css';
import Login from './Login';

Define the two functions that we'll be passing to the <Login> component. One function needs to handle the user typing in the input and update the username stored in the state. The other handles the form being submitted and will set the state to show that the user is logged in. Add these below the <App> component's constructor in src/App.js:

  handleLogin(event) {
    event.preventDefault();
    this.setState({ loggedIn: true });
  }
  handleUsernameChange(event) {
    this.setState({ username: event.target.value });
  }

In the constructor we need to initialise the state and bind these functions to the component:

  constructor(props) {
    super(props);
    this.state = {
      username: '',
      loggedIn: false
    };
    this.handleLogin = this.handleLogin.bind(this);
    this.handleUsernameChange = this.handleUsernameChange.bind(this);
  }

Now let's update the render function to show the username if the state says the user is logged in, and the <Login> component otherwise.

  render() {
    let loginOrChat;
    if (this.state.loggedIn) {
      loginOrChat = <p>Logged in as {this.state.username}</p>;
    } else {
      loginOrChat = (
        <Login
          handleLogin={this.handleLogin}
          handleUsernameChange={this.handleUsernameChange}
          username={this.state.username}
        />
      );
    }
    return (
      <div className="container">
        <div className="row mt-3 justify-content-center">{loginOrChat}</div>
      </div>
    );
  }

If your application is still running, return to the browser and you will see the login form. Otherwise start the app with npm run dev and open localhost:3000. Enter your name in the form and press enter or click "Sign in".

Hooking up Programmable Chat

Now we can use the username to generate an access token, and connect our logged in user with chat. Create a new file called src/ChatApp.js and open it up. We'll create a class based component for the chat app, so add the following boilerplate:

import React, { Component } from 'react';

class ChatApp extends Component {
}

export default ChatApp;

There are a few things we need to do in this component:

  • Retrieve an access token from the server and initialise the Twilio Chat client
  • Setup a chat channel and join it, loading any existing messages
  • Create a function to send a message
  • Render the KendoReact Conversational UI

Before any of that we'll need to import two modules; twilio-chat and the KendoReact conversationalUI. At the top of src/ChatApp.js add:

import React, { Component } from 'react';
import Chat from 'twilio-chat';
import { Chat as ChatUI } from '@progress/kendo-react-conversational-ui';

Let's set up some initial state in the constructor too. We'll need a list of messages, an error state in case anything goes wrong, and a boolean to show if the chat is loading, which will start as true.

class ChatApp extends Component {
  constructor(props) {
    super(props);
    this.state = {
      error: null,
      isLoading: true,
      messages: []
    };
  }
}

Getting an access token

The starter project is already setup to return a token when we pass an identity to the /chat/token endpoint. We'll use the fetch API to make the request as part of the componentDidMount lifecycle event. We use componentDidMount here as the React documentation tells us that this is a good place to load external data.

The response with the access token will be JSON, so we'll need to parse it using the response object's json method then once it's parsed, we can use the token to initialise the Chat client.

Creating the Chat client returns a promise so we can chain all these methods. Once the Chat client is created we will pass off to another method to finish the setup. We should also handle any errors with a catch method.

Add this code to the ChatApp class below the constructor:

  componentDidMount() {
    fetch('/chat/token', {
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      method: 'POST',
      body: `identity=${encodeURIComponent(this.props.username)}`
    })
      .then(res => res.json())
      .then(data => Chat.create(data.token))
      .then(this.setupChatClient)
      .catch(this.handleError);
  }

Write the method to handle the error. We'll set a message in the state and log the full error so we can debug if we have any trouble.

  handleError(error) {
    console.error(error);
    this.setState({
      error: 'Could not load chat.'
    });
  }

Setting up a Chat channel

We've initialised our Chat client with an access token but there's more to do. Once the promise resolves we need to use the new chat client to join a channel. As this is our first time through the process we'll check to see if the channel exists. If so, we'll attempt to join it; otherwise, we'll create it then join it.

Add the following setupChatClient method to the class:

  setupChatClient(client) {
    this.client = client;
    this.client
      .getChannelByUniqueName('general')
      .then(channel => channel)
      .catch(error => {
        if (error.body.code === 50300) {
          return this.client.createChannel({ uniqueName: 'general' });
        } else {
          this.handleError(error);
      }
    })
      .then(channel => {
       this.channel = channel;
       return this.channel.join().catch(() => {});
      })
      .then(() => {
        // Success!
      })
      .catch(this.handleError);
   }

We catch the error in the middle in case the channel doesn't exist (a 50300 error) and create the channel. Also, if joining a channel throws an error we catch it and do nothing. This handles the case when the user is already a member of the channel.

If everything works the code will get to the success comment. At this stage the channel has loaded, so we can set our state isLoading variable to false. We also need to load existing messages and set up a listener for new messages.

Replace the // Success! comment above with:

      .then(() => {
        this.setState({ isLoading: false });
        this.channel.getMessages().then(this.messagesLoaded);
        this.channel.on('messageAdded', this.messageAdded);
      })

Receiving Messages

We need to write the messagesLoaded and messageAdded methods we just referenced above, but before we do we need to consider the format that the KendoReact conversational UI wants the messages. We need to translate the message object from the format Twilio provides it to that which can be used by the conversational UI component.

Let's write a function that can take a message from the Chat service and return a message object for KendoReact:

  twilioMessageToKendoMessage(message) {
    return {
      text: message.body,
      author: { id: message.author, name: message.author },
      timestamp: message.timestamp
    };
  }

Now we can write the messagesLoaded and messageAdded methods. messagesLoaded runs when we first load the existing messages to a channel so we fill up state.messages with all the messages we receive.

  messagesLoaded(messagePage) {
    this.setState({
      messages: messagePage.items.map(this.twilioMessageToKendoMessage)
    });
  }

messageAdded will receive one message as its argument so we use the callback version of setState to add the message to the list. Note we also use the spread operator (...) to copy the existing messages into the new state.

  messageAdded(message) {
    this.setState(prevState => ({
      messages: [
        ...prevState.messages,
        this.twilioMessageToKendoMessage(message)
      ]
    }));
  }

Sending messages

We also need a function to send a message to a channel. This function will be called by the KendoReact Conversational UI when a user types a message in the message box and sends it by clicking the send button or pressing enter. To handle it, we need to send the message text onto the channel. Displaying the message will be handled by the existing messageAdded event we are listening to on the channel.

Add the following function to the ChatApp class:

  sendMessage(event) {
    this.channel.sendMessage(event.message.text);
  }

Tidying up and rendering the Conversational UI

We have some final parts to complete before we can see the chat in action. We should handle the component being unmounted. We can do this by shutting the chat client instance down.

  componentWillUnmount() {
    this.client.shutdown();
  }

The Conversational UI expects a user object, which we will create using our user identity. We also need to bind all of our callback functions to the component. Add the following to the constructor:

  constructor(props) {
    super(props);

    this.state = {
      error: null,
      isLoading: true,
      messages: []
    };
    this.user = {
      id: props.username,
      name: props.username
    };

    this.setupChatClient = this.setupChatClient.bind(this);
    this.messagesLoaded = this.messagesLoaded.bind(this);
    this.messageAdded = this.messageAdded.bind(this);
    this.sendMessage = this.sendMessage.bind(this);
    this.handleError = this.handleError.bind(this);
  }

Rendering the chat

Now we have everything in place we can render the Conversational UI. Create a render method in src/ChatApp.js that handles the various states of the component. If there are errors or if the chat is still loading, we will render a message, otherwise we will render the KendoReact Conversational UI component, passing the user object, the messages and the callback method to be run when the user sends a message.

  render() {
    if (this.state.error) {
      return <p>{this.state.error}</p>;
    } else if (this.state.isLoading) {
      return <p>Loading chat...</p>;
    }
    return (
      <ChatUI
        user={this.user}
        messages={this.state.messages}
        onMessageSend={this.sendMessage}
        width={500}
      />
    );
  }

Lastly we need to render this entire component from the <App> component. Import the <ChatApp> component at the top of src/App.js.

import React, { Component } from 'react';
import Login from './Login';
import ChatApp from './ChatApp';
import '@progress/kendo-theme-material/dist/all.css';

Now update the render function of the <App> component to return the <ChatApp> component when the user is logged in.

render() {
  let loginOrChat;
  if (this.state.loggedIn) {
    loginOrChat = <ChatApp username={this.state.username} />;
  } else {
    loginOrChat = (
      <Login
        handleLogin={this.handleLogin}
        handleUsernameChange={this.handleUsernameChange}
        username={this.state.username}
      />
    );
  }
  return (
    <div className="container">
      <div className="row mt-3">{loginOrChat}</div>
    </div>
  );

Reload the app, login and start chatting. You can open another browser window and login with a different name to see the messages going back and forth.

This is just the start

Twilio Programmable Chat is a powerful SDK for chatting and KendoReact's Conversational UI makes it really easy to display the chat in a React application. Most of the work we had to do was generating an access token and setting up the Twilio Chat. Once we'd written a couple of functions that translated the messages from Twilio to KendoReact and from KendoReact to Twilio the UI just fell into place.

You can get all the code for this application in the GitHub repo.

Check out the KendoReact documentation for other features of this UI, such as suggested actions, useful when the other side is a bot, and message attachments, for ways to display media messages or other views, like lists or carousels, within your chat.

The KendoReact Conversational UI is also available for jQuery, Angular and Vue if you prefer a different framework, and there are plenty of other useful components you could use to build your application.

Have you used KendoReact before? Or are you building chat into your app and on the lookout for a sweet UI? Let me know what you think in the comments or on Twitter at @philnash.

Discussion (13)

Collapse
zeluspudding profile image
Andrew Agostini • Edited

Great article @phil Nas! I found your writing style a lot easier to follow than other coding tutorials. Kudos to your clean and concise flow.

I do have a question - Do we need to fill in the TWILIO_TWIML_APP_SID to get this tutorial to work? I did but I'm not sure what to set the Messaging Request URL to.

At any rate, I get an error that reads:

Proxy error: Could not proxy request /chat/token from localhost:3000 to http://127.0.0.1:3001.
See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (ECONNREFUSED).

It seems like the server at 127.0.0.1:3001 isn't starting but I'm not sure why. Any guesses?

Collapse
philnash profile image
Phil Nash Author

Thanks very much!

You do not need to fill in the TWILIO_TWIML_APP_SID env variable, you only need the TWILIO_ACCOUNT_SID, TWILIO_API_KEY, TWILIO_API_SECRET and TWILIO_CHAT_SERVICE_SID.

When you start the application, are you running npm run dev? That should start the front end on localhost:3000 and the server on localhost:3001.

Let me know if that helps.

Collapse
zeluspudding profile image
Andrew Agostini • Edited

Yes, I have been starting it with npm run dev. When that didn't work I tweaked the localhost:3001 proxy to be http://127.0.0.1:3001/ and several other variations (even switching up the port number) according to suggestions on Stackoverflow and github, but those tries didn't pan out either. I would share other error messages to help you help me but unfortunately all I get is what I already posted above.

I also tried standing this up using your completed code (per chance I fudged something between pastes) but couldn't get that to work either. That threw a different error but I'm unable to share that one until this evening when I'm back home.

Thread Thread
philnash profile image
Phil Nash Author

Hmm, this is weird. Let me know what error you're getting from the completed project and hopefully we can get that fixed.

Also, for the one you worked on from the article, can you try the individual commands npm run server to start the server and npm start to build the front end and see if that helps or shows you any more errors?

Thread Thread
zeluspudding profile image
Andrew Agostini • Edited

Hey, thanks for following up!

I just checked and got this error:
'run-p' is not recognized as an internal or external command,
operable program or batch file.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! react-express-starter@0.1.0 dev:
run-p server start
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the react-express-starter@0.1.0 dev script.

Seems to be related to not having npm-run-all. However, when I tried installing that using npm install npm-run-all I got another error saying:
npm ERR! 404 Not Found - GET https://registry.npmjs.org/event-stream/-/event-stream-3.3.6.tgz
npm ERR! 404
npm ERR! 404 'event-stream@3.3.6' is not in the npm registry.
npm ERR! 404 You should bug the author to publish it (or use the name yourself!)
npm ERR! 404 It was specified as a dependency of 'twilio-chat-kendo-react'

...So stuck.

In a nice turn of events, however, I discovered TalkJS and got it working between two codepens in about 5 minutes. So I might be tinkering with that more.

It really strikes me that Twilio doesn't provide chat UI elements... like Stripe does with credit card forms. I know you're team has released Twilio Flex but I can't help but feel that a standalone chat element could only do good to sell more Twilio (it's not like it would canabalize your market for Flex).

At any rate, thank you for looking into this with me. I really do appreciate it.

Thread Thread
philnash profile image
Phil Nash Author

Ah, ok! This is an issue I've updated on a bunch of other projects, but not got to this one yet. This broke because of event-stream was compromised and then got yanked from the registry.

I've pushed updates to the package.json, so you should be able to pull the latest and try again.

Thread Thread
zeluspudding profile image
Andrew Agostini

Very interesting and unfortunate about event-stream (I hadn't known). I'm excited to try you're updated app :)

Changing the topic a bit, do you know of a site for checking whether an npm package is compromised.. something like Have I Been Pwnd? I couldn't find one in 30 seconds of googling...

Thread Thread
philnash profile image
Phil Nash Author

Compromised packages tend to be removed as soon as npm finds out about it, so there's not much need for a service like that.

There are services for checking whether a package has a vulnerability in. You can run npm audit or use something like Snyk to achieve this.

Collapse
enriquemorenotent profile image
Enrique Moreno Tent

Cool article, well written and complete, even though for some reason I have not managed to build it using react hooks.

Collapse
philnash profile image
Phil Nash Author

This is a good point, I intend to update this with hooks at some point too. What's going wrong with hooks for you at the moment?

Collapse
enriquemorenotent profile image
Enrique Moreno Tent

For some reason, when "messageAdded" executes, the "messages" state appears as an empty array, making all the previous chat history disappear whenever I type a new message.

Could it be that ChatUI mutates the array? I have no idea why that happens. I spent hours trying to find out, but I couldnt.

Collapse
enriquemorenotent profile image
Enrique Moreno Tent • Edited

Here is my component ChatApp written with hooks

pastebin.com/uVekpL41

Thread Thread
philnash profile image
Phil Nash Author

I'll try to have a play with this today. The ChatUI really shouldn't mutate the array. Will see what I can find.

Forem Open with the Forem app