Build a Chat App with Twilio Programmable Chat and React.js

November 25, 2020
Written by
Huzaima Khan
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

chatapp.png

This tutorial will teach you how to build a chat app in React using Twilio’s Programmable Chat API. After finishing this tutorial, you will have developed a web app to chat with other participants.

Prerequisites

Getting started

React project setup

Create a new directory on your computer called chat-app and change directory into it:

mkdir chat-app
cd chat-app

Your new chat-app directory will house both the backend and frontend components of the app. To create a new React project for the frontend, run the following command in your terminal:

npx create-react-app react-chat

Change directory to the newly created React project folder, and start the scaffolded React app to make sure everything works:

cd react-chat
yarn start

After running the above commands, a browser window will open and you’ll see the React logo spinning:

react logo

Stop the server by pressing CTRL + C in your terminal. Now it’s time to install the dependencies that will be used throughout the app.

To install these packages, run the following command:

yarn add axios @material-ui/core @material-ui/icons react-router-dom twilio-chat

At this point, you’re done with the React project setup. It’s time to set up the backend.

Backend server setup

Twilio provides you with a backend quickstart so that you can get your backend running in no time. These starters are available in many different languages. This quickstart provides the code required to generate an Access Token, which you’ll need later to authorize the client side of your app.

For this tutorial, you will be using the Twilio SDK Starter Application for Node.js.

In your terminal, navigate back to your parent directory, chat-app.

cd ..

Clone the Twilio SDK Starter Application repository:

git clone https://github.com/TwilioDevEd/sdk-starter-node.git

After the repository is cloned, change directory to enter the folder:

cd sdk-starter-node

Install any dependencies required by the starter app:

yarn install 

Then, install the cors package to allow your React app to make requests of this backend app:

yarn install cors

Open the app.js file inside the sdk-starter-node folder. Look for the line:

const app = express();

Anywhere below this line, add the following code:

const cors = require('cors');
app.use(cors());

Save and close this file.

Finally, duplicate the provided .env.example file and rename it to make it your actual .env file. Run the following command from your command prompt to do that:

mv .env.example .env

In the next section you’ll gather your Twilio credentials and add them to your new .env file.

Twilio Account Setup

Head over to Twilio and create a free account if you don't have one already.

Once your account is ready, go to your Twilio Console and copy the Account SID as shown in the screenshot below. Paste it in your new .env file as the value for the TWILIO_ACCOUNT_SID environment variable, replacing the placeholder value.

Screeshot of the Twilio Console with Account SID circled

Next, you need to generate an API key for authentication purposes. Go to Settings → API Keys in the sidebar and click Create new API Key.


twilio API key setup

Give your API key any recognizable name, like ChatApp, and then click create Create API Key.

twilio API key setup

Copy the SID and SECRET and paste them as the values for TWILIO_API_KEY and TWILIO_API_SECRET in your .env file, respectively.


twilio API key setup

At this point, you’re done with general account settings, so you can move on to service-specific settings.

The next step is to create a new Chat Service. Go to the Programmable Chat Services section of the Twilio Console. Click the plus sign or Create button. You’ll be prompted to give your new chat service a name. After naming it, click Create again.

You’ll now be redirected to a configuration page for your new chat service. Copy the SERVICE SID for your service and paste it as the value for the TWILIO_CHAT_SERVICE_SID environment variable in your .env file.


Screenshot of chat service configuration page with service SID circled

Back in your command prompt, run the following command to start your local server on port 5000.

PORT=5000 yarn start

That’s it for the backend setup. Now it’s time to start fleshing out the React app.

The welcome screen component

The React portion of this project will feature four components: WelcomeScreen, ChatScreen, ChatItem, and Router. React Router will be used to create navigation inside the app, so that the ChatScreen component will be available on its own route: /chat.

The WelcomeScreen component serves as a lobby, where the user can enter their email address and the name of a chat channel to join. Their input will be passed to ChatScreen, where you’ll make use of the Twilio Chat client library to build the actual chat interface.

You’ll start with the WelcomeScreen component. Create a new file called WelcomeScreen.js inside the src folder of the React project (chat-app/react-chat). At the top of the file, add the necessary imports:

import React from "react";
import {
  Grid,
  TextField,
  Card,
  AppBar,
  Toolbar,
  Typography,
  Button,
} from "@material-ui/core";

Beneath the imports, create the class for the component with a constructor method that initializes the component’s state:

class WelcomeScreen extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      email: "",
      room: "",
    };
  }
}

export default WelcomeScreen;

Next, inside the WelcomeScreen class, beneath the constructor method, paste in the following code to create a method called login():

login = () => {
  const { email, room } = this.state;
  if (email && room) {
    this.props.history.push("chat", { room, email });
  }
}

The login() method is called after the user enters their email address and chat channel, and clicks the Login button. The JSX for these login elements will be added inside another method called render() shortly. After the user presses the Login button, the login() method redirects them to the /chat route.

Next, beneath the login() method, still inside the WelcomeScreen class, paste in the following code to create a method called handleChange():

handleChange = (event) => {
  this.setState({ [event.target.name]: event.target.value });
};

This method is used to create a two-way bind between the contents of an input field and the component’s associated state. For example, as the user types in the email input field, the email state is updated, which triggers a rerender. Upon rerender, the new value of the email input field is the value of the updated email state. This way, the email state is always representative of what’s been typed in the email input and the user has a seamless typing experience.

Now, for the render() method, paste the following code inside the WelcomeScreen class just before the closing bracket. This method contains all the JSX for the lobby screen and login elements.

render() {
  const { email, room } = this.state;
  return (
    <>
      <AppBar style={styles.header} elevation={10}>
        <Toolbar>
          <Typography variant="h6">
            Chat App with Twilio Programmable Chat and React
          </Typography>
        </Toolbar>
      </AppBar>
      <Grid
        style={styles.grid}
        container
        direction="column"
        justify="center"
        alignItems="center">
        <Card style={styles.card} elevation={10}>
          <Grid item style={styles.gridItem}>
            <TextField
              name="email"
              required
              style={styles.textField}
              label="Email address"
              placeholder="Enter email address"
              variant="outlined"
              type="email"
              value={email}
              onChange={this.handleChange}/>
          </Grid>
          <Grid item style={styles.gridItem}>
            <TextField
              name="room"
              required
              style={styles.textField}
              label="Room"
              placeholder="Enter room name"
              variant="outlined"
              value={room}
              onChange={this.handleChange}/>
          </Grid>
          <Grid item style={styles.gridItem}>
            <Button
              color="primary"
              variant="contained"
              style={styles.button}
              onClick={this.login}>
              Login
            </Button>
          </Grid>
        </Card>
      </Grid>
    </>
  );
}

Finally, below the WelcomeScreen class, before the export, paste in the following styles object.

const styles = {
  header: {},
  grid: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 },
  card: { padding: 40 },
  textField: { width: 300 },
  gridItem: { paddingTop: 12, paddingBottom: 12 },
  button: { width: 300 },
};

Save and close WelcomeScreen.js.

The chat screen component

Create a new file called ChatScreen.js inside the src folder of the React project. This file will house the ChatScreen component code.

At the top of ChatScreen.js, add the necessary package and file imports:

import React from "react";
import {
  AppBar,
  Backdrop,
  CircularProgress,
  Container,
  CssBaseline,
  Grid,
  IconButton,
  List,
  TextField,
  Toolbar,
  Typography,
} from "@material-ui/core";
import { Send } from "@material-ui/icons";
import axios from "axios";
import ChatItem from "./ChatItem";
const Chat = require("twilio-chat");

Below the package imports, create the ChatScreen class with a constructor method that initializes the state for the component:

class ChatScreen extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      text: "",
      messages: [],
      loading: false,
      channel: null,
    };

    this.scrollDiv = React.createRef();
  }
}

export default ChatScreen;

On line 12 in the code above, a React ref was created to give you access to the specific HTML element created by the component. This ref will be used shortly to enable scrolling to the bottom of the chat window when a new message is added.

Add utility functions

Now you’ll add a few utility functions to your ChatScreen component.

The joinChannel() function is responsible for joining the channel and subscribing to the event handler for the messageAdded event.

The handleMessageAdded() function appends the incoming messages to the component’s messages state.

The scrollToBottom() function scrolls the chat message list, using the ref created in the constructor method, so the user can see the latest message at the bottom of the chat message list.

To add these methods, copy and paste the following code beneath the constructor() method inside the ChatScreen component.

joinChannel = async (channel) => {
   if (channel.channelState.status !== "joined") {
    await channel.join();
  }

  this.setState({ 
      channel:channel, 
      loading: false 
  });

  channel.on("messageAdded", this.handleMessageAdded);
  this.scrollToBottom();
};


handleMessageAdded = (message) => {
  const { messages } = this.state;
  this.setState({
      messages: [...messages, message],
    },
    this.scrollToBottom
  );
};

scrollToBottom = () => {
  const scrollHeight = this.scrollDiv.current.scrollHeight;
  const height = this.scrollDiv.current.clientHeight;
  const maxScrollTop = scrollHeight - height;
  this.scrollDiv.current.scrollTop = maxScrollTop > 0 ? maxScrollTop : 0;
};

The componentDidMount() method

The next step is to create a componentDidMount() method inside your ChatScreen component. This is a special React lifecycle method, and is the crux of this chat app. This method is responsible for network requests, event handling, token acquisition and refresh, and channels and message management.

Add this method beneath the constructor() method inside the ChatScreen component:

componentDidMount = async () => {
  const { location } = this.props;
  const { state } = location || {};
  const { email, room } = state || {};
  let token = "";

  if (!email || !room) {
    this.props.history.replace("/");
  }
}

In the code above, you added validation to make sure that you actually have an email address and room name. If either of them are missing, you will redirect the user back to the welcome screen.

Acquire an Access Token

An Access Token is a credential used to identify and authenticate the client with Twilio's Chat Service. The backend quickstart app you configured earlier in this tutorial contains the code required to generate this Access Token, but for additional reference, you can review Generating an Access Token for Twilio Chat, Video, and Voice using Twilio Functions.

Tokens are short-lived and need to be refreshed upon expiry. The same endpoint is used for both acquiring a new token and refreshing a token. Add another utility function inside your ChatScreen component to fetch a token from your project’s backend:

getToken = async (email) => {
  const response = await axios.get(`http://localhost:5000/token/${email}`);
  const { data } = response;
  return data.token;
}

With the utility function in place, you can edit the componentDidMount() method to call this function. The call to getToken() is wrapped in a try/catch block, because without a valid token you cannot proceed. Add the highlighted lines to your componentDidMount() method:

componentDidMount = async () => {
  const { location } = this.props;
  const { state } = location || {};
  const { email, room } = state || {};
  let token = "";

  if (!email || !room) {
    this.props.history.replace("/");
  }

  this.setState({ loading: true });

  try {
    token = await this.getToken(email);
  } catch {
    throw new Error("Unable to get token, please reload this page");
  }
}

Initialize the Twilio Chat SDK

As soon as you obtain a token, you can initialize the Twilio Chat Client and put a token refresh mechanism in place. Twilio provides two events to help you manage token expiration: tokenAboutToExpire and tokenExpired. To add event listeners for these two events, copy and paste the highlighted lines beneath the existing code inside your componentDidMount() method.


componentDidMount = async () => {
  ...

  const client = await Chat.Client.create(token);

  client.on("tokenAboutToExpire", async () => {
    const token = await this.getToken(email);
    client.updateToken(token);
  });

  client.on("tokenExpired", async () => {
    const token = await this.getToken(email);
    client.updateToken(token);
  });
}

Create or join a channel

Once the chat client is initialized, you can create a new chat channel or join an existing channel. To join an existing channel, fetch the channel resource from Twilio by using the SDK method getChannelByUniqueName() and passing to it the room name provided by the user.

In case that the channel doesn’t exist, an exception will be thrown. If it does exist, the method will return the channel resource, and from there, the channel can be joined.

To manage the possibility of an exception, fetching and joining the channel can be done in a try block. If the fetch fails, the accompanying catch block can be used to create and then join the channel.

If the client has joined an already existing channel, it should then get any existing messages associated with the channel.

Messages can be fetched by calling the SDK method getMessages() on the channel resource after the channelJoined event is emitted on the client. The component’s messages state can be updated with the returned messages.

To implement this functionality, add the highlighted lines to the end of your componentDidMount() method inside the ChatScreen component:


componentDidMount = async () => {
  …

  client.on("channelJoined", async (channel) => {
    // getting list of all messages since this is an existing channel
    const messages = await channel.getMessages();
    this.setState({ messages: messages.items || [] });
    this.scrollToBottom();
  });

  try {
    const channel = await client.getChannelByUniqueName(room);
    this.joinChannel(channel);
  } catch(err) {
    try {
      const channel = await client.createChannel({
        uniqueName: room,
        friendlyName: room,
      });
  
      this.joinChannel(channel);
    } catch {
      throw new Error("Unable to create channel, please reload this page");
    }
  } 
}

This completes your componentDidMount() method. Next, you’ll learn how to send new messages to a channel.

Send messages to a channel

To enable message sending in your application, you’ll create a method called sendMessage(). This message will call the SDK method sendMessage() on the channel object and pass to it the message typed by your user. Inside the ChatScreen class, copy and paste the following code:

sendMessage = () => {
  const { text, channel } = this.state;
  if (text) {
    this.setState({ loading: true });
    channel.sendMessage(String(text).trim());
    this.setState({ text: "", loading: false });
  }
};

This method will be called when the user clicks the send button after typing a new message.

Component rendering

Any existing messages in the channel are accessible in the component's messages state. The following render() method will map over these messages to display them to your user. Each individual message will be mapped to its own ChatItem component. You’ll create this component in the next section.

In addition to displaying existing and newly sent messages, the ChatScreen component’s render() method will render a text field where the user can type a message along with a button for sending that message.

Add the following code to the end of the ChatScreen component before the closing bracket:

 render() {
  const { loading, text, messages, channel } = this.state;
  const { location } = this.props;
  const { state } = location || {};
  const { email, room } = state || {};

  return (
    <Container component="main" maxWidth="md">
      <Backdrop open={loading} style={{ zIndex: 99999 }}>
        <CircularProgress style={{ color: "white" }} />
      </Backdrop>

      <AppBar elevation={10}>
        <Toolbar>
          <Typography variant="h6">
            {`Room: ${room}, User: ${email}`}
          </Typography>
        </Toolbar>
      </AppBar>

      <CssBaseline />

      <Grid container direction="column" style={styles.mainGrid}>
        <Grid item style={styles.gridItemChatList} ref={this.scrollDiv}>
          <List dense={true}>
              {messages &&
                messages.map((message) => 
                  <ChatItem
                    key={message.index}
                    message={message}
                    email={email}/>
                )}
          </List>
        </Grid>

        <Grid item style={styles.gridItemMessage}>
          <Grid
            container
            direction="row"
            justify="center"
            alignItems="center">
            <Grid item style={styles.textFieldContainer}>
              <TextField
                required
                style={styles.textField}
                placeholder="Enter message"
                variant="outlined"
                multiline
                rows={2}
                value={text}
                disabled={!channel}
                onChange={(event) =>
                  this.setState({ text: event.target.value })
                }/>
            </Grid>
            
            <Grid item>
              <IconButton
                style={styles.sendButton}
                onClick={this.sendMessage}
                disabled={!channel}>
                <Send style={styles.sendIcon} />
              </IconButton>
            </Grid>
          </Grid>
        </Grid>
      </Grid>
    </Container>
  );
}

Finally, beneath the ChatScreen class, before the export, add the highlighted styles object.


class ChatScreen extends React.Component {
  …
}

const styles = {
  textField: { width: "100%", borderWidth: 0, borderColor: "transparent" },
  textFieldContainer: { flex: 1, marginRight: 12 },
  gridItem: { paddingTop: 12, paddingBottom: 12 },
  gridItemChatList: { overflow: "auto", height: "70vh" },
  gridItemMessage: { marginTop: 12, marginBottom: 12 },
  sendButton: { backgroundColor: "#3f51b5" },
  sendIcon: { color: "white" },
  mainGrid: { paddingTop: 100, borderWidth: 1 },
};

export default ChatScreen;

The chat item component

Create a new file called ChatItem.js in the src folder inside your React project and paste the following code in it:

import React from "react";
import { ListItem } from "@material-ui/core";

class ChatItem extends React.Component {
  render() {
    const { message, email } = this.props;
    const isOwnMessage = message.author === email;

    return (
      <ListItem style={styles.listItem(isOwnMessage)}>
        <div style={styles.author}>{message.author}</div>
        <div style={styles.container(isOwnMessage)}>
          {message.body}
          <div style={styles.timestamp}>
            {new Date(message.dateCreated.toISOString()).toLocaleString()}
          </div>
        </div>
      </ListItem>
    );
  }
}

const styles = {
  listItem: (isOwnMessage) => ({
    flexDirection: "column",
    alignItems: isOwnMessage ? "flex-end" : "flex-start",
  }),
  container: (isOwnMessage) => ({
    maxWidth: "75%",
    borderRadius: 12,
    padding: 16,
    color: "white",
    fontSize: 12,
    backgroundColor: isOwnMessage ? "#054740" : "#262d31",
  }),
  author: { fontSize: 10, color: "gray" },
  timestamp: { fontSize: 8, color: "white", textAlign: "right", paddingTop: 4 },
};

export default ChatItem;

This component receives a message object through its props and renders the message body with other details, like its timestamp and author.

One thing that might catch your eye is the isOwnMessage variable. This variable is a boolean that reflects whether or not your client user is the author of the message. This variable allows you to appropriately style client-authored messages in order to differentiate between message senders. In the above code snippet, I applied different backgroundColor and alignItems styles based on whether the message was sent from this client or not.

Configuring React Router

Now that you have all the screens ready to roll, you need a way to tell React about which component needs to be shown on which route. For this, you will make use of the react-router-dom package that you installed earlier.

Create a new file called Router.js inside the src folder of the React project. Paste the following code in that file:

import React from "react";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import WelcomeScreen from "./WelcomeScreen";
import ChatScreen from "./ChatScreen";

function Router() {
  return (
    <BrowserRouter>
      <Switch>
        <Route exact path="/chat" component={ChatScreen} />
        <Route path="/" component={WelcomeScreen} />
      </Switch>
    </BrowserRouter>
  );
}

export default Router;

The above code tells React that whenever someone hits the /chat route, the ChatScreen component should be rendered, otherwise the user should see the WelcomeScreen component.

After your Router component is ready, you need to tell React to use it for routing. Inside the src folder of the React project, find the App.js file that was created automatically when you scaffolded the React app earlier. Replace the contents of that file with the following:

import React from 'react';
import Router from './Router';

function App() {
  return (
    <Router />
  );
}

export default App;

This code tells React to use your Router component for routing purposes.

Testing

In a new command prompt tab or window, navigate to the root of your React project, react-chat, and start the local server by running yarn start. Before starting the app, make sure your backend project is still running on localhost:5000.

Open the browser and head over to http://localhost:3000/ (replace the port with your own port if you’re not running it on 3000). You should be greeted with the welcome screen.

Chat App with Twilio Programmable Chat and React

Enter your email address and the name of a channel you wish to join (it will create the channel if it does not exist already) and click the login button. This will take you to the chat window where any existing messages will appear. In a second browser window, visit http://localhost:3000/ again and enter a different email address but the same room name. This will allow you to test out the chat app as two different users.

Chat App with Twilio Programmable Chat and React

Conclusion

In this article, you have learned how to build a chat app using React and Twilio Programmable Chat. For more info, check out the Twilio Programmable Chat Docs. You can find the complete React web app code on this GitHub repository. Stay tuned, we’ve more content coming up.

Huzaima is a software engineer with a keen interest in technology. He is always on the lookout for experimenting with new technologies. Other than this, he is passionate about aviation and travel. You can follow him on Twitter @HuzaimaKhan.