/ Tutorials

Tutorial: Build a React.js Chat App

Adding chat functionality to your web app can be crucial for enabling easy communication between your users. Whether you have an online marketplace, social network or a collaboration tool, chat lets your users connect in a flexible and personal way, without having to leave the app.

In this tutorial we're going to build a React.js chat application using Scaledrone, a tool that makes realtime communication really easy. The app will work similarly to apps like Messenger, WhatsApp or LINE.

This app will also be cross-platform compatible with our iOS and Android chat tutorials!

Setting Up

We'll start by creating a new React app using create-react-app, an official tool for getting started with React projects.

In order to use create-react-app you need Node.js. Follow the instructions on the link to install if you haven't already. Once you've done that, creating our starter app is really easy. Open Terminal and navigate to a folder where you'd like your app to exist. Type in the following command:

npx create-react-app app-name
cd app-name

Note: The app-name can be anything you like.

Open the newly created app-name folder in your favorite editor.

Next, we'll need to include the CSS to make sure our app looks nice. Replace the contents of App.css with the contents of this file.

Now we're ready to get started!

Showing Messages

To make our chat app we'll use three components.

  1. An App component which will manage sending and receiving messages and rendering the inner components.
  2. A Messages component which will be a list of messages.
  3. An Input component with a text field and button so that we can send our messages.

create-react-app has already made an App component for you which we'll modify later. Let's start with the Messages component.

Create a new file in your src folder called Messages.js. Create a new component in this file called Messages, and add a render function like in the following code:

import {Component} from "react";
import React from "react";

class Messages extends Component {
  render() {
    const {messages} = this.props;
    return (
      <ul className="Messages-list">
        {messages.map(m => this.renderMessage(m))}
      </ul>
    );
  }
}

export default Messages;

The component will receive the messages it should display as a prop from the App component. We'll render a list containing each message. The list items themselves will contain the sender's name and avatar, as well as the text of the message. Add the following function to the component:

renderMessage(message) {
  const {member, text} = message;
  const {currentMember} = this.props;
  const messageFromMe = member.id === currentMember.id;
  const className = messageFromMe ?
    "Messages-message currentMember" : "Messages-message";
  return (
    <li className={className}>
      <span
        className="avatar"
        style={{backgroundColor: member.clientData.color}}
      />
      <div className="Message-content">
        <div className="username">
          {member.clientData.username}
        </div>
        <div className="text">{text}</div>
      </div>
    </li>
  );
}

This will render the JSX for each individual message.

Now that we have the Messages component, we need to make sure the App component actually renders it. Head over to App.js and add an import of the Messages component to the top of the file:

import Messages from "./Messages";

I mentioned before that we'll show usernames and avatars in our app. Usually you would have a login screen where you would get this information, but for the purposes of this tutorial we'll generate random names and colors for the avatar. Add the following top-level functions to the top of the file:

function randomName() {
  const adjectives = ["autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark", "summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter", "patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue", "billowing", "broken", "cold", "damp", "falling", "frosty", "green", "long", "late", "lingering", "bold", "little", "morning", "muddy", "old", "red", "rough", "still", "small", "sparkling", "throbbing", "shy", "wandering", "withered", "wild", "black", "young", "holy", "solitary", "fragrant", "aged", "snowy", "proud", "floral", "restless", "divine", "polished", "ancient", "purple", "lively", "nameless"];
  const nouns = ["waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning", "snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter", "forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook", "butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly", "feather", "grass", "haze", "mountain", "night", "pond", "darkness", "snowflake", "silence", "sound", "sky", "shape", "surf", "thunder", "violet", "water", "wildflower", "wave", "water", "resonance", "sun", "wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper", "frog", "smoke", "star"];
  const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
  const noun = nouns[Math.floor(Math.random() * nouns.length)];
  return adjective + noun;
}

function randomColor() {
  return '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16);
}

The usernames are generated by adding a random adjective to a noun, leading to cool-sounding names like "hiddensnow" or "throbbingfrog". The colors are generated by generating a random hex number, while the member's ID is generated by generating a random string.

Now that we have those functions, we can use them to set our initial state in the App component. Add the following to the top of App:

state = {
  messages: [
    {
      text: "This is a test message!",
      member: {
        color: "blue",
        username: "bluemoon"
      }
    }
  ],
  member: {
    username: randomName(),
    color: randomColor()
  }
}

Our initial state will be one test image and a randomly generated member.

Next, update the contents of the render method to the following:

render() {
  return (
    <div className="App">
      <Messages
        messages={this.state.messages}
        currentMember={this.state.member}
      />
    </div>
  );
}

Run the app by navigating to the project folder in Terminal and typing in:

npm start

Head over to localhost:3000 and take a look at the first message in your chat app!

React Chat Bubble

Adding Input

Now that we have a way to display our messages, we need a way to type them! Create a new file in the src folder called Input.js. In this file, add a new component called Input:

import {Component} from "react";
import React from "react";

class Input extends Component {
  state = {
    text: ""
  }
}

export default Input;

This component will have a text field to enter the message and a button to send it. We'll keep track of the currently entered text in our state. Add the following render method to the component:

render() {
  return (
    <div className="Input">
      <form onSubmit={e => this.onSubmit(e)}>
        <input
          onChange={e => this.onChange(e)}
          value={this.state.text}
          type="text"
          placeholder="Enter your message and press ENTER"
          autofocus="true"
        />
        <button>Send</button>
      </form>
    </div>
  );
}

Whenever the textfield's value changes, we'll update our state to match. Add the following function to the class:

onChange(e) {
  this.setState({text: e.target.value});
}

This will trigger a render whenever the text field changes. Finally, we need to handle sending the message. Add the following function to the class:

onSubmit(e) {
  e.preventDefault();
  this.setState({text: ""});
  this.props.onSendMessage(this.state.text);
}

To send the message we'll use a callback inside the component's props which we'll receive from App. We need to prevent the default behavior so our app doesn't refresh every time a new message is sent. We'll also update the state so that we clear the textfield.

Just like we did with Messages, we need to make sure App will render Input. Head over to App.js and add the import for your new component to the top of the file:

import Input from "./Input"

Next, update the contents of the render method to show the Input component:

render() {
  return (
    <div className="App">
      <Messages
        messages={this.state.messages}
        currentMember={this.state.member}
      />
      <Input
        onSendMessage={this.onSendMessage}
      />
    </div>
  );
}

Finally, we need to handle sending the message. Add the following function to App:

onSendMessage = (message) => {
  const messages = this.state.messages
  messages.push({
    text: message,
    member: this.state.member
  })
  this.setState({messages: messages})
}

For now we'll just add it onto the messages array in our state. If you head back to your web browser you should see an input field. If you type in a message and hit return it should display in the list.

React First Working Chat

Right now our app is looking a bit bland. To add a bit of flare, we'll add a header with the title of our app. Update the render inside App.js to the following:

render() {
  return (
    <div className="App">
      <div className="App-header">
        <h1>My Chat App</h1>
      </div>
      <Messages
        messages={this.state.messages}
        currentMember={this.state.member}
      />
      <Input
        onSendMessage={this.onSendMessage}
      />
    </div>
  );
}

We just added a new div with the title of our app. Head over to the browser. You should see a nice, grey header above your messages list.

React Chat With Header

Now it's looking like a real chat app! Congratulations! Now let's get our hands dirty and connect everything to Scaledrone.

Hooking It Up

Thankfully Scaledrone makes the business logic of making a chat app really simple. But before we start using it we need to make sure Scaledrone is included in our app.

Open public/index.html. Inside the HTML head tag, add the following line to the top:

<script src='https://cdn.scaledrone.com/scaledrone.min.js'></script>

This will make sure Scaledrone's code is included in our app. We're almost ready to start using it, but we need to do one more thing first: create a Scaledrone channel.

To successfully connect to Scaledrone you need to get your own channel ID from the Scaledrone's dashboard. If you haven't already, head over to Scaledrone.com and register an account. Once you've done that, go to your dashboard and click the big green +Create Channel button to get started. You can choose Never require authentication for now. Note down the channel ID from the just created channel, you'll need it in a bit.

Head back to App.js and add a constructor to the class:

constructor() {
  super();
  this.drone = new window.Scaledrone("YOUR-CHANNEL-ID", {
    data: this.state.member
  });
  this.drone.on('open', error => {
    if (error) {
      return console.error(error);
    }
    const member = {...this.state.member};
    member.id = this.drone.clientId;
    this.setState({member});
  });
}

This will instantiate a new instance of Scaledrone. Make sure to replace the string with the channel ID you got earlier. Because we're loading the script inside our HTML head tag, the script's contents will be attached to the global window object, which is where we'll get a Scaledrone instance.

We'll also pass Scaledrone the data for the currently logged in member. If you have additional data about the user or the client this is a good way to supply that data, instead of having to send it with each message.

Next, we need to connect to a room. A room is a group of users that we can send messages to. You listen to those messages by subscribing to a room of a specific name. Add the following code to the end of the constructor:

const room = this.drone.subscribe("observable-room");

We'll subscribe to a room.

Note: You might have noticed that we named our name Scaledrone room observable-room. You can name the room anything you want, a single user can actually connect to an infinite amount of rooms for all sorts of application scenarios. However, in order for messages to contain the info of the sender, you need to prefix the room name with "observable-". Read more...

We also need to know when the messages arrive, so we'll subscribe to the "data" event on the room. Add the following code to the end of the constructor:

room.on('data', (data, member) => {
  const messages = this.state.messages;
  messages.push({member, text: data});
  this.setState({messages});
});

When we get a new message, we'll add the message's text as well as the client data to the our state. Next, we need to modify onSendMessage to publish a new message to all the other users in the room:

onSendMessage = (message) => {
  this.drone.publish({
    room: "observable-room",
    message
  });
}

Scaledrone makes this a simple matter of calling the publish function with the data.

Finally, we'll remove the test message from our app's state so that the initial state looks like the following:

state = {
  messages: [],
  member: {
    username: randomName(),
    color: randomColor()
  }
}

If you go to your web browser now the behavior will look the same: by sending a new message it should appear in the list. The difference is that this time it's actually being sent to Scaledrone. You can open the app in another tap and chat with yourself. :)

React Chat Window

And We're Done!

Congrats, you have built a React chat app! With Scaledrone, you get to focus on making your UI beautiful, and on the functionality of your app, without worrying about the back end. You can find the full source code on GitHub.

Here are some feature ideas to improve on this app using Scaledrone's APIs:

  • Display who's online
  • Show when a user is typing
  • Send messages and attachments
  • Authentication

You can find the full source code or run the working prototype on GitHub. If you have any questions or feedback feel free to contact us.

Send realtime data to your users
We take care of complex realtime infrastructure problems so you can focus on what you enjoy - building awesome apps
Build realtime features now