In this tutorial, you will learn how to build a group chat app similar to Slack with React and CometChat.

Here’s a preview of what you’ll build:

As you can see, users can “login” then create, join, and communicate in channels.

Coming into this article, you probably have at least some experience with React but what is CometChat?

Good question, I am glad you asked!

CometChat is a service which enables you to build chat features into your application without the need to worry about back-end code. This way, you can focus on the features unique to your application (in this case, team communication).

You’ll learn more about CometChat as you follow along so without any further adieu, let’s get started.

Create a CometChat Pro App

First things first, you’ll need to create a CometChat app.

Head to the dashboard (create a free account if you haven’t already), and you’ll see this page:

Enter your app name, I called mine “Group Chat”, then click the + button to create your app.

Once created, click Explore to view the app:

From the left sidebar, click API Keys then Create API Key:

Call your key what you like, I named mine “Group Chat” after the app, but make sure to select Auth Only from the Scope dropdown.

Take note of your API Key and App id because you’ll need them later in your code:

API keys are a core concept in CometChat, which you can read more about here.

With your app in place, let’s move on to the client and setup React.

Setting up React

Nowadays, there are many ways to scaffold a React application. To scaffold ours, we’ll use a popular tool called create-react-app:

npx create-react-app react-group-chat
cd react-group-chat

Once the command has finished running, install react-router-dom too:

npm install react-router-dom --save

Your directory structure should look like this:

Create a configuration file

With our CometChat app and React boilerplate in place, we should create a configuration file to hold the APP_ID and API_KEY that we just created.

Create a file in the src directory called settings.js and paste the following:

const API_ID = "{api_ID}";
const API_KEY = "{api_KEY}";

export { API_ID, API_KEY };

Remember to replace "{API_ID}" and "{API_KEY}" with your own values from the dashboard.

Next, let’s install and initialise CometChat.

To install CometChat, run the following command:

npm install @CometChat-pro/chat --save

Once installed, head to App.js where we will initialise CometChat from the constructor:

import { CometChat } from "@CometChat-pro/chat.js";
import {API_ID} from "./config.js";

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

    CometChat.init(API_ID).then(
      hasInitialized => {
        console.log("Initialization completed successfully", hasInitialized);
      },
      error => {
        console.log("Initialization failed with error:", error);
      }
    );
  }

It’s useful to initialise CometChat in a container component (in our case, App) so we can make it accessible throughout our React application.

Run the app from the command-line with npm start.

If the connection to CometChat is successful (in other words, if you copied your credentials correctly), you should see a success message like: “Initialization completed successfully true” in your console.

If you don’t see this message something is wrong. The most common cause for problems is that your credentials are incorrect so make sure to double-check.

Create Your App Components

Our chat application is going to have a Login component, a Dashboard container component, a Channel component and ChatBox component:

The truth is, most of the time will need container components that take responsible for providing and managing data and behavior to their child components.

In our case, the Channel and ChatBox components are children of and managed by the Dashbaord container component. The differences between container and children components is nuanced, you can read more about the difference here

I like to organize my components into directories. So, for each component we’ll create a directory. The content of each directory will be an index.js, index.css.

In the end, your application structure will look a little like this:

But perhaps we are getting ahead of ourselves… Before delving too far into the code, let me give you an overview of how the application will work.

A user will log into the application with their username (from the Login component).

To keep things simple, we won’t allow the user to sign up. Instead, they can pick a username from the demo users provided by CometChat namely, “superhero1”, “superhero2”, “superhero3”, "superhero4”, and - you guessed it “superhero5”.

You can create additional users from the dashboard (handy for testing and while developing) or using the create use API.

After a user logs in, they will be redirected to what we will call the Dashboard component. Dashboard is a container component that renders and manages the Chatbox and the Channel components. Users can click on any channel to start chatting in that channel.

Login Component

Naturally, the first component we should create is the login component to identify users:

Create a new folder called component/Login to hold files related to the Login component.

Then create a file called component/Login/index.js:

import React, { Component } from "react";
import { Redirect } from "react-router-dom";
import { CometChat } from "@CometChat-pro/chat";
import { API_KEY } from "../../config.js";
import "./index.css";
import Loading from "./loading.svg";

export default class Login extends Component {
  constructor() {
    super();
    this.login = this.login.bind(this);
    this.renderRedirect = this.renderRedirect.bind(this);
    this.handleUserInput = this.handleUserInput.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.state = {
      redirect: false,
      userName: "",
      isLoading: false,
      error: ""
    };
  }
  handleSubmit(e) {
    e.preventDefault();
    this.login();
    this.setState({ isLoading: true, error: "" });
  }
  handleUserInput(e) {
    this.setState({ userName: e.target.value.toUpperCase() });
  }
  renderRedirect = () => {
    return <Redirect to="/dashboard" />;
  };
  login() {
    // Becareful of exposing your API key here.
    // It can be dangerous if it gets into the hands of unauthorize users
    CometChat.login(this.state.userName, API_KEY).then(
      user => {
        console.log("Login Successful:", { user });
        this.setState({ redirect: true });
      },
      error => {
        console.log("Login failed with exception:", { error });
        this.setState({
          error: "Login failed, please enter a valid username",
          isLoading: false
        });
      }
    );
  }
  render() {
    return (
      <React.Fragment>
        <div className="login">
          <h4>Welcome to Your React Chat App</h4>
          {!this.state.redirect ? "" : this.renderRedirect()}
          <div>
            <form onSubmit={this.handleSubmit}>
              <div>
                <input
                  className="groupname"
                  placeholder="Enter Your Username"
                  onChange={this.handleUserInput}
                />
              </div>
              <button className="button modal-button">Login</button>
            </form>
            <div className="error">{this.state.error}</div>
            <div>
              {this.state.isLoading ? (
                <p className="loading">
                  <img alt="loading" src={Loading} />
                </p>
              ) : (
                ""
              )}
            </div>
          </div>
          <div className="signup-text">
            <p>Need an account?</p>
            <p>
              Create one from the{" "}
              <a href="https://app.CometChat.com/" target="blank">
                CometChat Pro dashboard
              </a>
            </p>
            <p>
              or use one of our test usernames: superhero1, superhero2,
              superhero3 to login
            </p>
          </div>
        </div>
      </React.Fragment>
    );
  }
}

In the same directory, create another file called index.css and paste the contents of lindex.css.

“Paste this code” isn’t exactly educational but regrettably, styling is a bit outside the scope of this tutorial. Our focus is on React and CometChat so while I will share a link to the file, I won’t explain it in this tutorial.

You’ll also want to download loading.svg and save it in the same directory. When the user logs in, we’ll show them this loading spinner to let them know things are working in the background.

So, what is happening here?

At the top of index.js, we import our API_KEY as well as react-router-dom, which will enable us to redirect the user to the chat if their login is successful.

The most important code happens in a function called login where we authenticate the user:

login() {
    // Becareful of exposing your API key here.
    // It can be dangerous if it gets into the hands of unauthorize users
    CometChat.login(this.state.userName, API_KEY).then(
      user => {
        this.setState({ redirect: true });
        //do something else here.
      },
      error => {
        console.log("Login failed with exception:", { error });
        this.setState({
          error: "Login failed, please enter a valid username",
          isLoading: false
        });
      }
    );
  } 

If login returns a user (in other words, if the login was successful), we’ll update our redirect state to true which indicates to the render function that we should redirect the user to the Dashboard:

this.setState({ redirect: true });

If something went wrong, we show an error to the user and hide the spinner:

this.setState({
    error: "Login failed, please enter a valid username",
    isLoading: false
});

In the render function, we render the loading spinner according to isLoading:

{this.state.isLoading ? (
  <p className="loading">
    <img alt="loading" src={Loading} />
  </p>
) : (
  ""
)}

Dashboard container component

Create a new folder called component/Dashboard to hold files related to the Dashboad component.

Then create a file called component/Dashboard/index.js:

import React, { Component } from "react";
import Chatbox from "../Chatbox";
import Channels from "../Channels";

export default class Dashboard extends Component {
  constructor(props) {
    super(props);
    this.state = {
      channelUID: "",
      isShowMessages: false
    };
    this.updateState = this.updateState.bind(this);
  }
  // recieve event from props and update the state with the data
  updateState(channel) {
    this.setState({ channelUID: channel, isShowMessages: true }, () => {
      return { channelUID: channel, isShowMessages: true };
    });
  }
  render() {
    return (
      <React.Fragment>
        <Channels updateState={this.updateState} />
        <Chatbox state={this.state} />
      </React.Fragment>
    );
  }
}

In the same directory, create index.css and paste the contents of this index.css file.

Our Dashboard component is a container component, which means it’s job is to fetch and manage data for it’s children components, in this case: Channels and ChatBox:

<Channels updateState={this.updateState} />
<Chatbox state={this.state} />

The Channels component, outlined in red is to show the user channels to pick from.

The ChatBox component, outlined in green is where chat messages are input and rendered:

The Dashboard coordinates these components so, for example, if someone clicks on a channel, Dashboard knows abut it and updates ChatBox accordingly.

See this function:

updateState(channel) {
    this.setState({ channelUID: channel, isShowMessages: true }, () => {
      return { channelUID: channel, isShowMessages: true };
    });
  }

It’s responsible for updating the state of the dashboard with the channeluid from the Channle’s component.

In the updateState function, we are also updating the isShowMessages property. We set it to true to indicate that a group has been selected and the ChatBox should render the messages for the selected group.

With all of that said, we haven’t actually yet defined the Channels or ChatBox components. Let’s do that next.

Channels Component

Create a new folder called component/Channels to hold files related to the Login component.

Then create a file called component/Channels/index.js:

import React, { Component } from "react";
import { CometChat } from "@CometChat-pro/chat";
import { Link } from "react-router-dom";
import "./index.css";
export default class Groups extends Component {
  constructor(props) {
    super(props);
    this.channelsLimit = 30;
    this.state = {
      channels: [],
      isChannelActive: ""
    };
  }
  componentDidMount() {
  /*
  Here we are pulling previous chatmessages from the api immediately the channel is activated
  */ 
    this.groupsRequest = new CometChat.GroupsRequestBuilder()
      .setLimit(this.channelsLimit)
      .build();
    this.groupsRequest.fetchNext().then(
      channels => {
        /* groupList will be the list of Group class */
        this.setState({ channels });
      },
      error => {
        console.log("channels list fetching failed with error", error);
      }
    );
  }
  selectGroup(channelID) {
    this.password = "";
    this.groupType = CometChat.GROUP_TYPE.PUBLIC;
    this.props.updateState(channelID);
    CometChat.joinGroup(channelID, this.groupType, this.password).then(
      channel => {
        console.log(" Joined channels successfully:", channel);
      },
      error => {
        console.log("You are already a member of this group");
      }
    );
  }
  render() {
    return (
      <React.Fragment>
        <div className="group">
          <div className="groupList">
            <ul>
              {this.state.channels.map(channels => (
                <li
                  key={channels.guid}
                  onClick={this.selectGroup.bind(this, channels.guid)}
                >
                  <div className="groupName"> # {channels.name}</div>
                </li>
              ))}
            </ul>
          </div>
          <div className="createGroup">
            <button className="createGroupBtn button">
              <Link className="a" to="/createchannel">
                Create A Channel
              </Link>
            </button>
          </div>
        </div>
      </React.Fragment>
    );
  }
}

In the same directory, create another file called index.css and paste the contents of this file.

In index.js, We fetch the list of channels easily using this function:

this.groupsRequest = new CometChat.GroupsRequestBuilder()
      .setLimit(this.channelsLimit)
      .build();
    this.groupsRequest.fetchNext().then(
      channels => {
        /* groupList will be the list of Group class */
        this.setState({ channels });
      },
      error => {
        console.log("channels list fetching failed with error", error);
      }
    );
  }

Here, we create an instance of the GroupRequestBuilder which, in turn, returns a list of groups or, as we call them in our app, channels. Even though CometChat uses the term groups, we use channels - just like Slack channels.

ChatBox Component

Create a new folder called component/ChatBox to hold files related to the Chatbox component.

Then create a file called component/ChatBox/index.js:

import React, { Component } from "react";
import { CometChat } from "@CometChat-pro/chat";
import "./index.css";
export default class Chatbox extends Component {
  constructor(props) {
    super(props);
    this.state = {
      receiverID: this.props.state.channelUID,
      messageText: null,
      channelMessages: [],
      user: {}
    };
    this.receiverID = this.state.receiverID;
    this.messageType = CometChat.MESSAGE_TYPE.TEXT;
    this.receiverType = CometChat.RECEIVER_TYPE.GROUP;
    this.messagesLimit = 30;
    this.send = this.send.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleMessageInput = this.handleMessageInput.bind(this);
    this.fetchNewMessages = this.fetchNewMessages.bind(this);
    this.getUser = this.getUser.bind(this);
    this.newMessageListener = this.newMessageListener.bind(this);
  }
  componentDidUpdate() {
    if (this.props.state.channelUID !== this.state.receiverID) {
      this.fetchNewMessages();
    }
  }
  shouldComponentUpdate(nextProps, nextState) {
    if (this.state.recieverID === this.props.state.channelUID) {
      return false;
    }
    return true;
  }
  componentDidMount() {
    this.getUser();
    this.newMessageListener();
  }
  fetchNewMessages() {
    this.messagesRequest = new CometChat.MessagesRequestBuilder()
      .setGUID(this.props.state.channelUID)
      .setLimit(this.messagesLimit)
      .build();
    this.messagesRequest.fetchPrevious().then(
      messages => {
        this.setState(
          {
            channelMessages: messages,
            receiverID: this.props.state.channelUID
          },
          () => {
            return {
              channelMessages: messages,
              receiverID: this.props.state.channelUID
            };
          }
        );
        this.scrollToBottom();
      },
      error => {
        console.log("Message fetching failed with error:", error);
      }
    );
  }
  send() {
    let textMessage = new CometChat.TextMessage(
      this.state.receiverID,
      this.state.messageText,
      this.messageType,
      this.receiverType
    );
    CometChat.sendMessage(textMessage).then(
      message => {
        console.log("Message sent successfully:", message);
      },
      error => {
        console.log("Message sending failed with error:", error);
      }
    );
  }
  scrollToBottom() {
    const chat = document.querySelectorAll(".chat")[0];
    chat.scrollTop = chat.scrollHeight;
  }
  handleSubmit(e) {
    e.preventDefault();
    this.send();
    e.target.reset();
  }
  handleMessageInput(e) {
    this.setState({ messageText: e.target.value });
  }
  // Get the current logged in user
  getUser() {
    CometChat.getLoggedinUser().then(
      user => {
        this.setState({ user: user }, () => {
          return { user: user };
        });
        return { user };
      },
      error => {
        console.log("error getting details:", { error });
        return false;
      }
    );
  }
  newMessageListener() {
    this.listenerID = "groupMessage";
    CometChat.addMessageListener(
      this.listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: textMessage => {
          this.setState(({ channelMessages }) => {
            return { channelMessages: [...channelMessages, textMessage] };
          });
        }
      })
    );
  }
  renderMessages() {
    return this.props.state.isShowMessages
      ? this.state.channelMessages.map(data => (
          <div>
            {/* Render loggedin user chat at the right side of the page */}
            {this.state.user.uid === data.sender.uid ? (
              <li className="self" key={data.id}>
                <div className="msg">
                  <p>{data.sender.uid}</p>
                  <div className="message"> {data.data.text}</div>
                </div>
              </li>
            ) : (
              // render loggedin users chat at the left side of the chatwindow
              <li className="other" key={data.id}>
                <div className="msg">
                  <p>{data.sender.uid}</p>
                  <div className="message"> {data.data.text} </div>
                </div>
              </li>
            )}
          </div>
        ))
      : "";
  }
  renderChatInputBox() {
    return this.props.state.isShowMessages ? (
      <div className="chatInputWrapper">
        <form onSubmit={this.handleSubmit}>
          <input
            className="textarea input"
            type="text"
            placeholder="Type a message..."
            onChange={this.handleMessageInput}
          />
        </form>
        <div className="emojis" />
      </div>
    ) : (
      ""
    );
  }
  render() {
    return (
      <React.Fragment>
        <div className="chatWindow">
          <ol className="chat">{this.renderMessages()}</ol>
          {this.renderChatInputBox()}
        </div>
      </React.Fragment>
    );
  }
}

And, like I am sure you expect by now, create another file in the same directory called index.css and paste the code from this file.

That is a lot of code to digest, let me break it down.

At the top of index.js, and as per usual, we import React, CometChat and our component styles:

import React, { Component } from "react";
import { CometChat } from "@CometChat-pro/chat";
import "./index.css";

Then, in the constructor we set the receiverID based on a prop provided by the Dashboard container component:

this.state = {
      receiverID: this.props.state.channelUID,
      messageText: null,
      channelMessages: [],
      user: {}
    };

In this case, the receiverID denotes the group ID to send and receive messages to and from.

The dashbaord can update this any time, for example, if a new channel is selected.

In componentDidUpdate, we fetch historical messages using the MessagesRequestBuilder:

componentDidUpdate() {
        if (this.props.state.channelUID !== this.state.receiverID) {
          this.messagesRequest = new CometChat.MessagesRequestBuilder()
          .setGUID(this.props.state.channelUID)
          .setLimit(this.messagesLimit)
          .build();
        this.messagesRequest.fetchPrevious().then(
          messages => {
            this.setState(
              {
                channelMessages: messages,
                receiverID: this.props.state.channelUID
              },
              () => {
                return {
                  channelMessages: messages,
                  receiverID: this.props.state.channelUID
                };
              }
            );
            this.scrollToBottom();
          },
          error => {
            console.log("Message fetching failed with error:", error);
          }
        );
        }
      }

Likewise, when the component mounts, we begin to listen for new messages in real-time:

componentDidMount() {
    this.getUser();
    this.newMessageListener();
}

newMessageListener() {
    this.listenerID = "groupMessage";
    CometChat.addMessageListener(
      this.listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: textMessage => {
          this.setState(({ channelMessages }) => {
            return { channelMessages: [...channelMessages, textMessage] };
          });
        }
      })
    );
  }

To send new messages, we simply use the sendMessage function:

send() {
    let textMessage = new CometChat.TextMessage(
      this.state.receiverID,
      this.state.messageText,
      this.messageType,
      this.receiverType
    );
    CometChat.sendMessage(textMessage).then(
      message => {
        console.log("Message sent successfully:", message);
      },
      error => {
        console.log("Message sending failed with error:", error);
      }
    );
  }

Create Channel Component

We can provide our users with some default channels, but we also want to empower them to create their own.

To enable this, create a new folder called component/CreateGroup to hold files related to the CreateGroup component.

Then create a file called component/CreateGroup/index.js:

import React, { Component } from "react";
import { Redirect, Link } from "react-router-dom";
import { CometChat } from "@CometChat-pro/chat";
import "./index.css";
export default class CreateChannel extends Component {
  constructor(props) {
    super(props);
    this.groupType = CometChat.GROUP_TYPE.PUBLIC;
    this.password = "";
    this.handleChannelName = this.handleChannelName.bind(this);
    this.handleChannelUID = this.handleChannelUID.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.createChannel = this.createChannel.bind(this);
    this.state = {
      channelUID: "",
      channelName: "",
      isCreated: false,
      error: false
    };
  }
  handleChannelName(e) {
    this.setState({ channelName: e.target.value });
    console.log(e.target.value);
  }
  handleChannelUID(e) {
    this.setState({ channelUID: e.target.value });
    console.log(e.target.value);
  }
  handleSubmit(e) {
    e.preventDefault();
    this.createChannel();
  }
  renderRedirect = () => {
    return <Redirect to="/dashboard" />;
  };
  createChannel() {
    this.channel = new CometChat.Group(
      this.state.channelUID,
      this.state.channelName,
      this.groupType,
      this.password
    );
    CometChat.createGroup(this.channel).then(
      channel => {
        console.log("channel created successfully:", channel);
        this.setState({ isCreated: true });
      },
      error => {
        console.log("channel creation failed with exception:", error);
        this.setState({ error: true });
      }
    );
  }
  render() {
    return (
      <React.Fragment>
        <div className="modalcreate">
          <div id="open-modal" className="modal-window">
            <div>
              <Link to="#" title="Close" className="modal-close">
                Close
              </Link>
              <h1>Enter Channel Name</h1>
              <form onSubmit={this.handleSubmit}>
                <div>
                  <input
                    className="groupname"
                    onChange={this.handleChannelName}
                    placeholder="Channel Name"
                  />
                </div>
                <div>
                  <input
                    className="groupname"
                    onChange={this.handleChannelUID}
                    placeholder="Channel UID"
                  />
                </div>
                <button className="button modal-button">Create Channel</button>
              </form>
              <p>{this.state.isCreated ? this.renderRedirect() : ""}</p>
              <p>{this.state.error ? "Channel creation failed" : ""}</p>
            </div>
          </div>
        </div>
      </React.Fragment>
    );
  }
}

In the same directory, create another file called index.css and paste the contents of GroupChat/index.css from GitHub.

This component boils down to making a cal to createGroup:

createChannel() {
      this.channel = new CometChat.Group(
      this.state.channelUID,
      this.state.channelName,
      this.groupType,
      this.password
    );
    CometChat.createGroup(this.channel).then(
      channel => {
        console.log("channel created successfully:", channel);
        this.setState({ isCreated: true });
      },
      error => {
        console.log("channel creation failed with exception:", error);
        this.setState({ error: true });
      }
    );
  }

Generally, the main function of our components is to bridge the cap between the UI and CometChat. We don’t need to write custom logic for basic chat functionality because CometChat handles it all behind the scene.

Conclusion

There you have it. Your group chat app with react and CometChat Pro.

If you made it this far, you are the real MVP.

The entire code for this tutorial can be found here.

There is so much you can do with CometChat Pro, register to get your API credentials and let me know what you built with it.

If you need a hand, let us know — we are always ready to help.