Building Chat Interfaces Using JavaScript and React

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

Chat interface built with React

If you’re integrating a service like Twilio’s Programmable Chat into your website, you’re going to need an interface for users to interact with. Let’s use React and a suite of modern development tools to create an application for submitting and displaying chat messages.

Designing Our Interface

Before we begin building our chat interface, we should have an idea of what we want to create. Our chat application will have a container with a list of messages, and a form for writing and submitting messages. A simple design might look something like this:

As we build our our interface, we’ll identify any isolated piece of the UI that might contain its own state and behavior. Those will be our initial React components. In this simple design, the two most distinct areas are the message list and the message form.

Setting Up Our Development Environment

Developers working with React commonly use a modern build pipeline that includes Webpack and Babel. We’re going to use Create React App, a tool maintained by Facebook, to expedite our setup. While it’s possible to configure these tools individually, Create React App speeds things up by generating a complete React application with just one command.

With node 6 or later and npm installed, we can install create-react-app using the following command:

client
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   └── favicon.ico
│   └── index.html
│   └── manifest.json
└── src
    └── App.css
    └── App.js
    └── App.test.js
    └── index.css
    └── index.js
    └── logo.svg
    └── registerServiceWorker.js

Let’s step into this new directory, start the application, and see what our default state looks like.

cd chat
npm start

When we start the application, our build process will continue to run in our terminal and our browser will open a new tab. There, you’ll see a “Welcome to React” message with a spinning React logo and a little information on how to start changing this application.

From this point on, we’ll leave this build server running so that any changes we make are reflected in the browser.

Our First Component

In our application, App.js is our top level component. If we open up this file in our editor, we’ll see the contents of the component that was displayed in the browser.

// ./src/App.js

import React, { Component } from 'react'
import logo from './logo.svg'
import './App.css'

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    )
  }
}

export default App

Any changes to this file should be reflected immediately in the browser. Let’s remove the logo import, replace the content in the render method with a simple “Ahoy World!” message, and save the file.

// ./src/App.js

import React, { Component } from 'react'
import './App.css'

class App extends Component {
  render() {
    return (
      <div className="App">
        <h1>Ahoy World!</h1>
      </div>
    )
  }
}

export default App

While we’re making changes to this component, let’s clean up the App.css file and setup some of the styles we’ll be using in our chat interface. First, replace all of the existing styles with the CSS below. Our new styles will immediately be reflected in the browser as soon as we save the file.

/* ./src/App.css */

.App {
  border: 1px solid #ccc;
  border-radius: 3px;
  height: 400px;
  width: 600px;
  margin: .5em;
  font-size: 13px;
  display: flex;
  flex-direction: column;
  text-align: center;
}

Displaying Messages

In our wireframe, we identified a list of messages as one of the distinct components of our interface. We’ll begin implementing the message list by importing a new MessageList component at the top of App.js, adding a reference to this component in the render method, and updating our application’s layout styles:

// ./src/App.js

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

// ...

  render() {
    return (
      <div className="App">
        <MessageList />
      </div>
    )
  }
}

export default App

/* ./src/App.css */
// …


.App .MessageList {
  box-sizing: border-box;
  flex: 1;
}

If we save these files now, we’ll see an error in our build process that looks something like this:

Failed to compile.

./src/App.js
Module not found: Can't resolve './MessageList' in '~/chat/client/src'

As we build our chat interface, this build process and the corresponding browser tab will continue to notify us of any issues with our application. To fix this error, we’ll need to create a new file at ./src/MessageList.js. We’ll also create a stylesheet for this component at ./src/MessageList.css:

// ./src/MessageList.js

import React, { Component } from 'react'
import './MessageList.css'

class MessageList extends Component {
  render() {
    return (
      <div className="MessageList">
        <div>Connecting...</div>
        <div><span className="author">You:</span> Hello!</div>
        <div><span className="author">Them:</span> Hey there!</div>
      </div>
    )
  }
}

export default MessageList

/* ./src/MessageList.css */

.MessageList {
  overflow-y: auto;
  padding: 4px;
}

When we save these files our error will go away, but now we’re just rendering a few static messages. Ideally we’ll want this component to accept a list of messages as a prop, then iterate over that list rendering each one. In order to define the types of data we expect for our props we’ll want to install the prop-types library.

npm install --save prop-types

Now, we’ll import the PropTypes object and define two static variables for our expected and default props. Well expect our messages prop to be an array of objects, and the default value of messages will be an empty array.

// ./src/MessageList.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './MessageList.css'

class MessageList extends Component {
  static propTypes = {
    messages: PropTypes.arrayOf(PropTypes.object)
  }

  static defaultProps = {
    messages: [],
  }

 // …
}

In the render method we’ll iterate over our new array of messages using map and return an html element containing the message content.

// ./src/MessageList.js

  // ...
  render() {
    return (
      <div className="MessageList">
        {this.props.messages.map((message, i) => (
          <div>
            {message.author && (
              <span className="author">{message.author}:</span>
            )}
            {message.body}
          </div>
        ))}
      </div>
    )
  }

  // ...

If we save the MessageList component now we shouldn’t have any errors. But, without any messages being passed into our component we’re only rendering an empty div. We want our top level App component to be responsible for our application’s state, so we’ll need to make a few changes there in order to display our messages again.

In the constructor of our App component, we’ll begin defining our initial state. Right now our state will include a single key messages, which will be an array of message objects. The three things we might ultimately need to know about our messages are the name of the author, the body of the message, and whether or not this is a message from ourselves so that we can style our own messages differently from other users.

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

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      messages: [
        { body: "Connecting..." },
        { author: "You", body: "Hello!", me: true },
        { author: "Them", body: "Hey there!"  },
      ],
    }
  } 
  // ...
}

Next, we’ll update our render method so that it passes these messages to our MessageList component.

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

class App extends Component {
  // …
  render() {
    return (
      <div className="App">
        <MessageList messages={this.state.messages} />
      </div>
    )
  }
}

Styling Messages

This is a great start, but we need to style these messages, and we want to display system messages and user messages differently from our own. Those differences in style require a little logic, so let’s create another component for an individual message. We’ll import a new Message component, then pass the properties of our message object as props.

// ./src/MessageList.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Message from './Message'
import './MessageList.css'

// …

  render() {
    return (
      <div className="MessageList">
        {this.props.messages.map((message, i) => (
          <Message key={i} {...message} />
        ))}
      </div>
    )
  }

// ...

We’ll probably see another error in our build process telling us that the Message component doesn’t exist. Let’s create the Message component, define it’s required PropTypes give it a render method that matches the markup we were previously using in the MessageList component.

// ./src/Message.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'

class Message extends Component {
  static propTypes = {
    author: PropTypes.string,
    body: PropTypes.string.isRequired,
    me: PropTypes.bool,
  }

  render() {
    return (
      <div className="Message">
        {this.props.author && (
          <span className="author">{this.props.author}:</span>
        )}
        {this.props.body}
      </div>
    )
  }
}

export default Message

To style the messages differently, we’ll want to apply different CSS classes. We could write a few conditionals that build up a class name string, but there’s actually a great library that can help us conditionally join classes together called classnames.

npm install --save classnames

The classnames library lets us pass both string arguments, and an object with keys and boolean values to the function classNames, and it returns a concatenated string consisting of string values, and any object keys that had truthy values. In our Message component, we’ll want to conditionally add two classes to each message. If our message has no author, we’ll assume it’s a system message and give it the class log, and we’ll also give it the class me if the me prop passed to our component is truthy. 

// ./src/Message.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classNames from "classnames"
import './Message.css'

class Message extends Component {
  static propTypes = {
    author: PropTypes.string,
    body: PropTypes.string.isRequired,
    me: PropTypes.bool,
  }

  render() {
    const classes = classNames('Message', {
      log: !this.props.author,
      me: this.props.me
    })

    return (
      <div className={classes}>
        {this.props.author && (
          <span className="author">{this.props.author}:</span>
        )}
        {this.props.body}
      </div>
    )
  }
}

export default Message

Finally, we’ll create a Message.css file with the corresponding styles for our classnames.

/* ./src/Message.css */

.Message {
  line-height: 1.5em;
}

.Message.log {
  color: #bbb;
}

.Message .author {
  font-weight: bold;
  color: #888;
  margin: 0 5px 0 0;
}

.Message.me .author {
  color: #b6191e;
}

Our messages are looking a lot nicer. We’ve got distinct styles for system logs, our messages, and other users’ messages. Now, we need to add the ability to create messages.

Creating New Messages

First, we’ll import and render a new MessageForm component in App.js. We’ll pass this component a prop called onMessageSend, and define a method that should be called when a new message is created. This method, handleNewMessage, will set the state of the app’s messages to all of the previous messages, plus our new message with the author set to “Me”, the me attribute set to true, and whatever text is passed into the method as the body of the message.

// ./src/App.js

import React, { Component } from 'react'
import MessageForm from './MessageForm'
import MessageList from './MessageList'

// …

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

  // …
  
  handleNewMessage = (text) => {
    this.setState({
      messages: [...this.state.messages, { me: true, author: "Me", body: text }],
    })
  }

  // ...

  render() {
    return (
      <div className="App">
        <MessageList messages={this.state.messages} />
        <MessageForm onMessageSend={this.handleNewMessage} />
      </div>
    )
  }
}

We’ll also update our App.css file again with layout styles for the MessagesForm component.

/* ./src/App.css */
// …

.App .MessageForm {
  background: #eee;
  border-top: 1px solid #ccc;
  border-radius: 0 0 3px 3px;
  box-sizing: border-box;
  flex: 0 0 30px;
}

Our MessageForm component will require one function prop, onMessageSend, which we’re passing when rendering this component from App.js. In our render method, we define a simple form with a text input and a submit button. When that form is submitted, it will call the method handleFormSubmit which will prevent the default form submit, pass the current value of the text input to the onMessageSend function, then reset the value of the text input to an empty string.

// ./src/MessageForm.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './MessageForm.css'

class MessageForm extends Component {
  static propTypes = {
    onMessageSend: PropTypes.func.isRequired,
  }

  componentDidMount = () => {
    this.input.focus()
  }

  handleFormSubmit = (event) => {
    event.preventDefault()
    this.props.onMessageSend(this.input.value)
    this.input.value = ""
  }

  render() {
    return (
      <form className="MessageForm" onSubmit={this.handleFormSubmit}>
        <div className="input-container">
          <input
            type="text"
            ref={(node) => (this.input = node)}
            placeholder="Enter your message…"
          />
        </div>
        <div className="button-container">
          <button type="submit">
            Send
          </button>
        </div>
      </form>
    )
  }
}

export default MessageForm

Again, we’ll also create the stylesheet for this component.

/* ./src/MessageForm.css */

.MessageForm {
  display: flex;
  background: #ccc;
}

.MessageForm .input-container {
  flex: 1;
  margin: 1px;
}

.MessageForm .button-container {
  flex: 0 0 6em;
  margin: 1px 1px 1px 0;
}

.MessageForm input, .MessageForm button {
  background: #fff;
  border-radius: 3px;
  border: 1px solid #ccc;
  box-sizing: border-box;
  font-size: inherit;
  height: 100%;
  outline: none;
  width: 100%;
}

.MessageForm input {
  padding: 0 0 0 4px;
}

With the addition of the MessageForm component, we now have a working one-sided chat application! If we begin typing and submitting messages, they’ll appear in the message log.

One final issue you might have noticed is that if we send enough messages to fill up our message list window, they begin scrolling off of the screen. With a  small addition to our MessageList component, we can resolve this.

Right after our defaultProps definition in ./src/MessageList.js, we’ll add a componentDidUpdate lifecycle hook to our application that will scroll to the bottom of the window whenever the component is updated. In the render method for that component, we’ll also add a callback ref that defines this.node in our component.

// ./src/MessageList.js
// ...

  componentDidUpdate = () => {
    this.node.scrollTop = this.node.scrollHeight
  }

  render() {
    return (
      <div className="MessageList" ref={(node) => (this.node = node)}>
        {this.props.messages.map((message, i) => (
          <Message key={i} {...message} />
        ))}
      </div>
    )
  }

// ...

What Did We Build?

Using Create React App, we generated a new application, then developed components for submitting and displaying new messages which we stored in our App component’s state. If we now wanted to integrate a service like Twilio’s Programmable Chat into this interface, we would just need to change the behavior of our App component’s handleNewMessage method.

For an idea of how to implement Twilio’s Programmable Chat, I recommend taking a look at Twilio’s JavaScript Quickstart guide.

Code for this article is available on github. If you’re interested in learning more about the build tools we used, take a look at Create React App, Webpack, and Babel.

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