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
- Node.js
- A JavaScript package manager of your choice; npm or yarn
- A free Twilio account (if you sign up through this link we'll both get $10 in Twilio credit)
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:
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.
- Axios - HTTP client for all the API calls
- Material UI - React UI components
- React Router - React browser routing
- Twilio Chat SDK - Twilio Chat client library
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.
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.
Give your API key any recognizable name, like ChatApp, and then click create Create API Key.
Copy the SID and SECRET and paste them as the values for TWILIO_API_KEY
and TWILIO_API_SECRET
in your .env file, respectively.
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.
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.
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.
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.

Learn how to use Cohere AI and Twilio to build a WhatsApp bot that can generate lyrics in the style of your favorite artist.

Learn how to create a Twilio WhatsApp service on Node.js that can tap into the vast knowledge and personalities of famous individuals using ChatGPT

Learn to infer reply content after multiple messages in WhatsApp using Twilio Programming Messaging and the Content API

In this tutorial, you will use the Google Maps API and Twilio WhatsApp to discover nearby restaurants

Learn how to implement 2FA with Twilio Verify in Node.js, including how to generate a Twilio API key and how to integrate Twilio Verify into your application. This tutorial will also provide tips and best practices for ensuring the security of your users' accounts.

Learn how to build a stock screener using Danfo.js that checks for breakout prices and notifies you via SMS.