Chat applications have evolved and are now a major part of modern communication tools. Its applications range from communications, team engagements, products reports and feedbacks as well as technical support systems. It has become a major part of our daily lives, hence, the need to make it even more robust for users. Often while we chat, we feel the need to send more than just texts, maybe share images, documents or even contacts. In this post, we’ll show you how to build a CometChat application that does just that. With this app, we take your chat app development experience up a notch by building to send/receive not just texts but files too.

Audience and learning goal

This tutorial is best suited for developers who are looking to incorporate chat features and functionalities into an existing product or to build one from scratch. In the end, you will learn how to build a React chat application with text and file-sharing capabilities. You will also learn how to create and use CometChat API’s to build out working chat applications.

Before we proceed, here’s a visual representation of the functioning application we'll build.

Next, we will walk through the processes of building this app from scratch. However, you can jump straight to the code repository on Github if you wish to.

Prerequisites

To follow along with this tutorial, you will need:

  • Basic knowledge of React.js - a JavaScript front-end development framework for building intuitive user interfaces.
  • Familiarity with API’s and chat features will be an added advantage but not mandatory to follow along.
  • You also need to have Node and NPM installed on your development machine. If you don't have it, install it here.
  • Fair knowledge of React Hooks is necessary to clearly understand the snippets shared in this post. If you're new to React Hooks feel free to read this post first.
  • Finally, you will need to have a free account with CometChat, feel free to create one here.

Hold your horses ⚠️🐴!
To follow this tutorial or run the example source code you'll need to create a V1 application.

v2 will be out of beta soon at which point we will update this tutorial.

Getting started

First, create a React project and set up CometChat configurations. To do this, open your terminal or command prompt and run the command below:

npx create-react-app cometchat-app
cd cometchat-app

Install CometChat and Bootstrap
Next, let’s install CometChat into our app for easy chat build. Simply run this command:

npm install bootstrap @cometchat-pro/chat --save

If you’ve successfully installed all the dependencies, the dependencies section of your package.json file should look like this:

  "dependencies": {
    "@cometchat-pro/chat": "^1.4.3",
    "bootstrap": "^4.3.1",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-scripts": "3.0.1"
  }

The next thing we need to do is to configure CometChat to communicate with our React app. To do this, you will need to use the CometChat credentials in your CometChat dashboard. If you created an account at the beginning as instructed, navigate to your account dashboard and copy your API_KEY (Remember to keep it safe) and your APP_ID.

Next, create a config.js file within the src folder where we can define our application configurations:

// src/config.js
const config = {
  // Be sure to set the key scope to fullAccess
  appID: 'YOUR_APP_ID',
  apiKey: 'YOUR_API_KEY',
};
export default config;

Create the application components

Before we go any further, let's take a minute to understand the structure of our app. Basically, we'll have just two components — Login and Chat. When the app loads, we render the Login component so users can supply their credentials and log into the app. When the login process is successful, we render the chat component. With that said, let's go ahead and create these components.

Take the following steps:

  • In the src folder, create a new components folder
  • Navigate into the components folder and create the Login.js and Chat.js files

We'll update these files with the appropriate code contents as we progress through the post, in the meantime, let's initialize CometChat and render the components when the app loads.

Initialize CometChat

When the app loads, we need to initialize CometChat inside the entry file. This is important because we need to login users into our chat app and we can't do that unless CometChat is initialized. Open the App.js file in the project root directory and update it with the code below:

import React, {useState, useEffect} from 'react';
import 'bootstrap/dist/css/bootstrap.css';
import {CometChat} from '@cometchat-pro/chat';
import config from './config'
import './App.css';
import Login from './components/Login';
import Chat from './components/Chat';

const App =()=> {
  const [user, setUser] = useState(null);
  useEffect(()=>{
      CometChat.init(config.appID).then(
       () => {
        console.log("Initialization completed successfully");
        //You can now call login function.
       },
       error => {
        console.log("Initialization failed with error:", error);
        //Check the reason for error and take apppropriate action.
       }
      );
    
  })
  const renderApp = () => { 
    if (user) {
      return <Chat user={user} />;
    } else {
    return <Login setUser={setUser} />;
    }
  } 
  return (
    <div className='container'>
    {renderApp()}
    </div>
  );
}
export default App;

Here, we initialized CometChat with the appID we defined the config.js file earlier. We are also conditionally rendering the components — If a user exists (someone has logged in) render the Chat component. But if that is not the case, render the Login component.

Login component

Now that we have that functionality, let's go ahead and set up the Login component to handle the actual logging in of our users. Open the Login.js file and update it with the code below:

//src/components/Login.js

import React, { useState } from 'react';
import { CometChat } from '@cometchat-pro/chat';
import config from '../config';
const Login = props => {
    const [userID, setUserID] = useState('');
    const [isLoggingIn, setIsLoggingIn] = useState(false);
    const loginUser = event => {
        event.preventDefault();
        setIsLoggingIn(true);
        CometChat.login(userID, config.apiKey).then(
            User => {
                console.log('Login Successful:', { User });
                props.setUser(User);
            },
            error => {
                console.log('Login failed with exception:', { error });
                setIsLoggingIn(false);
            }
        );
    };
    return (
        <div className='row'>
            <div className='col-md-6 login-form mx-auto'>
                <h3>Login to Chat</h3>
                <p>To login and test out the chat, use any of the username's below or create your users from your CometChat dashboard <br></br>
                    "superhero1" <br></br>
                    "superhero2"<br></br>
                </p>
                <form className='mt-5' onSubmit={loginUser}>
                    <div className='form-group'>
                        <input
                            type='text'
                            name='username'
                            className='form-control'
                            placeholder='Your Username'
                            value={userID}
                            onChange={event => setUserID(event.target.value)}
                        />
                    </div>
                    <div className='form-group'>
                        <input
                            type='submit'
                            className='btn btn-primary btn-block'
                            value={`${isLoggingIn ? 'Loading...' : 'Login'}`}
                            disabled={isLoggingIn}
                        />
                    </div>
                </form>
            </div>
        </div>
    );
};
export default Login;

Here, we defined a userID variable to hold the username from the form when a user fills it. Then we use the supplied username and API key to log in the user. Once logged in, we update the User prop with the currently logged in user. This will then notify the App.js file to render the Chat component. Everything else going on here is just basic React form handling concepts.

Chat component

The Chat component comprises of other functionalities that we'll briefly talk about now. First when the component loads, we display a list of friends that the logged-in user can chat with. When a particular friend is selected from the list, we remove the list from the DOM and bring up the chatbox where the actual conversation will happen.

Fetch friends
First, let's implement the Friendslist function that will fetch and render all available friends for the logged-in user to select from. While performing the fetch request to get available friends, it's important to keep the user engaged with a Spinner or Progress Dialog. Let's install the React MD Spinner to help us handle that. Open your terminal and run the command:

  npm install react-md-spinner

That done, we can go ahead and implement the fetch request within the Chat.js file like so:

//src/components/Chat.js
import React, { useState, useEffect } from "react";
import MDSpinner from "react-md-spinner";
const limit = 30;
const MESSAGE_LISTENER_KEY = "listener-key";
let boxMessage = "Welcome to CometChat"

const FriendList = props => {
  //... TODO 0
  const { friends, friendisLoading, selectedFriend } = props;
  if (friendisLoading) {
    return (
      <div className="col-xl-12 my-auto text-center">
        <MDSpinner size="72" />
      </div>
    );
  } else {
    return (
      <ul className="list-group list-group-flush w-100">
        {friends.map(friend => (
          <li
            key={friend.uid}
            className={`list-group-item ${
              friend.uid === selectedFriend ? "active" : ""
              }`}
            onClick={() => props.selectFriend(friend.uid)}
          >
            {friend.name}
          </li>
        ))}
      </ul>
    );
  }
  // ... TODO 1 HERE
};
export default Chat;

Here, we've passed down three variables from props:
friends — Holds a list of the fetched friends.
friendisLoading — Holds the loading state of the request.
selectedFriend — Holds the selected friend from the rendered list.

While the fetch request is in progress, the friendisLoading variable will be true and the Spinner will be visible, when the request is completed and friends are loaded, we render a list of the fetched friends.

Like we mentioned earlier, when the user selects a friend to chat with (from the list of friends) we want to remove the list from the DOM since we won't be needing it anymore.

//src/components/Chat.js
// TODO 0 👇🏼👇🏼
  if (selectedFriend) {
    if (document.getElementById("friends")) {
      let friendsNode = document.getElementById("friends")
      friendsNode.parentNode.removeChild(friendsNode)
    }
  }
export default Chat;

Chat functionality
First, we'll perform the actual request to fetch the friends and update the values of the variables we've defined. This is also where we pass the updated values to props.

//src/components/Chat.js
import React, { useState, useEffect } from "react";
// ... TODO 1 👇🏼👇🏼
const Chat = ({ user }) => {
  const [friends, setFriends] = useState([]);
  const [selectedFriend, setSelectedFriend] = useState(null);
  const [chat, setChat] = useState([]);
  const [chatIsLoading, setChatIsLoading] = useState(false);
  const [friendisLoading, setFriendisLoading] = useState(true);
  const [message, setMessage] = useState("");
  const [file, setFile] = useState(null)
  const [placeholder, setPlaceholder] = useState("Type message ...")
  useEffect(() => {
    let usersRequest = new CometChat.UsersRequestBuilder()
      .setLimit(limit)
      .build();
    usersRequest.fetchNext().then(
      userList => {
        console.log("User list received:", userList);
        setFriends(userList);
        setFriendisLoading(false);
      },
      error => {
        console.log("User list fetching failed with error:", error);
      }
    );
    return () => {
      CometChat.removeMessageListener(MESSAGE_LISTENER_KEY);
      CometChat.logout();
    };
  }, []);

  //TODO 2 ... Next snippet goes Here

}
export default Chat;

Next, when the user selects a friend from the list, we want to fetch previous messages if any. We also want to add text and media message listeners to listen for incoming messages and update the Chat state accordingly. When new messages come in, we also want to scroll the view to the current message.

//src/components/Chat.js
const Chat = ({ user }) => {
  // ...TODO 2 👇🏼👇🏼
useEffect(() => {
    if (selectedFriend) {
      boxMessage = "You're chatting with " + selectedFriend
      let messagesRequest = new CometChat.MessagesRequestBuilder()
        .setUID(selectedFriend)
        .setLimit(limit)
        .build();
      messagesRequest.fetchPrevious().then(
        messages => {
          setChat(messages);
          setChatIsLoading(false);
          scrollToBottom();
        },
        error => {
          console.log("Message fetching failed with error:", error);
        }
      );
      CometChat.removeMessageListener(MESSAGE_LISTENER_KEY);
      CometChat.addMessageListener(
        MESSAGE_LISTENER_KEY,
        new CometChat.MessageListener({
          onTextMessageReceived: message => {
            console.log("Incoming Message Log", { message });
            if (selectedFriend === message.sender.uid) {
              setChat(prevState => [...prevState, message]);
              scrollToBottom();
            }
          },
          onMediaMessageReceived: message => {
            console.log("incoming media", { message });
            if (selectedFriend === message.sender.uid) {
              setChat(prevState => [...prevState, message]);
              scrollToBottom();
            }
          }
        })
      );
    }
  }, [selectedFriend]);
  
  // ... TODO 3 Here

}
export default Chat;

Here, when the user selects a friend, we update the boxMessage variable with the name of the selected friend. Then we make a request to fetch the previous messages between the two users if any. We've also attached a message listener to monitor incoming text and media messages to update the chat UI accordingly.
So far, we have used two functions that we haven't defined yet — selectFriend() and scrollToBottom(). We didn't forget, we are just getting to it and this is the time define them:

//src/components/Chat.js
const Chat = ({ user }) => {
  // TODO 3 👇🏼👇🏼
  const scrollToBottom = () => {
    let chatContainer = document.getElementById('chat-container')
    chatContainer && chatContainer.scrollTo(0, chatContainer.scrollHeight)
  };
  const selectFriend = uid => {
    setSelectedFriend(uid);
    setChat([]);
    setChatIsLoading(true);
  };
  // TODO 4 Here
}
export default Chat;

Next, let's create the Chatbox component. This component will be responsible for fetching the previous message history between users and also render incoming text and media messages to the view

//src/components/Chat.js

const ChatBox = props => {
  const { chat, chatIsLoading, user } = props;
  if (chatIsLoading) {
    return (
      <div className="col-xl-12 my-auto text-center">
        <MDSpinner size="72" />
      </div>
    );
  } else {
    return (
      <div className="col-xl-12">
        {chat.map(chat => (
          <div key={chat.id} className="message">
            <div
              className={`${
                chat.receiver !== user.uid ? "balon1" : "balon2"
                } p-3 m-1`}
            >
              {chat.type === "text" ? <div>{chat.text}</div> :
                <img class="img" src={chat.file} alt="chat media" />
              }
            </div>
          </div>
        ))}
      </div>
    );
  }
};
export default Chat;

Just like in the Friendlist component, we also rendered the Spinner when the component is performing the request to fetch previous messages.

Next, let's define the return() function for our Chat component to render the actual chat interface where users can type, pick/send files and messages:

//src/components/Chat.js
const Chat = ({ user }) => {
// ... TODO 5 Here !!

//... TODO 4 👇🏼👇🏼
 return (
    <div className="container-fluid">
      <div className="row">
        <div className="col-md-2" />
        <div className="col-md-8 h-100pr border rounded">
          <div id="parent" className="row">
            <div id="friends"
              className=" col-lg-8 col-xs-12 bg-light"
              style={{
                height: 658
              }}
            >
              <div className="row p-3">
                <h4>Select a friend to chat with</h4>
              </div>
              <div
                className="row ml-0 mr-0 h-75 bg-white border rounded"
                style={{ height: "100%", overflow: "auto" }}
              >
                <FriendList
                  friends={friends}
                  friendisLoading={friendisLoading}
                  selectedFriend={selectedFriend}
                  selectFriend={selectFriend}
                />
              </div>
            </div>
            <div
              className="col-lg-8 col-xs-12 bg-light"
              style={{ height: 658 }}>
              <div className="row p-3 bg-white">
                <h3>{boxMessage}</h3>
              </div>
              <div
                id="chat-container"
                className="row pt-5 bg-white"
                style={{ height: 530, overflow: "auto" }}
              >
                <ChatBox
                  chat={chat}
                  chatIsLoading={chatIsLoading}
                  user={user}
                />
              </div>
              <div
                className="row bg-light"
                style={{ bottom: 0, width: "100%" }}>
                <form className="row1 m-0 p-0 w-100" onSubmit={handleSubmit} >
                  <div className="col-9 m-0 p-1">
                    <input
                      id="text"
                      className=" in mw-100 border rounded form-control"
                      type="text"
                      onChange={event => {
                        setMessage(event.target.value);
                      }}
                      value={message}
                      placeholder={placeholder}
                    />
                  </div>
                  <div>
                    <span class="btn btn-outline-secondary rounded border w-100 btn-file">
                      <FontAwesomeIcon
                        icon={faPaperclip} />
                      <input type="file" id="img_file"
                        name="img_file" accept="image/x-png,image/gif,image/jpeg"
                        files={file} onChange={e => {
                          setFile(e.target.files[0])
                          setPlaceholder(e.target.files[0].name)
                        }} />
                    </span>
                  </div>
                  <div>
                    <button disabled={!file} type="button" onClick={sendFile} className="sfile btn btn-outline-secondary rounded border w-100"
                    > Send File
                    </button>
                  </div>
                </form>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
export default Chat;

Here, we are using Bootstrap classes to layout our chat UI. We rendered the Friendlist component first to present the user with a list of friends to select from. Then we rendered the Chatbox component to display the messages between the users. Finally, we created the form that holds the UI elements (Text Input, File Input, and Send File Button) that controls the chat functionality.

I'm sure you noticed that we used Font Awesome icons and you're probably wondering when we added that, well we haven't. 😃 Let's do that now. Back to your terminal, run the following command:

npm install @fortawesome/react-fontawesome @fortawesome/free-solid-svg-icons @fortawesome/fontawesome-svg-core

From the previous code snippet above, we passed in the handleSubmit() function to execute when the form is submitted. We also did the same with the Send File button. When the button is clicked, we call the sendFile() function. At the moment, we haven't defined either of those so let's do that now — TODO 5:

//src/components/Chat.js
const Chat = ({ user }) => {  
//... TODO 5 👇🏼👇🏼
const handleSubmit = event => {
    event.preventDefault();
    let textMessage = new CometChat.TextMessage(
      selectedFriend,
      message,
      CometChat.MESSAGE_TYPE.TEXT,
      CometChat.RECEIVER_TYPE.USER
    );
    console.log(textMessage);
    CometChat.sendMessage(textMessage).then(
      message => {
        console.log("Message sent successfully:", message);
        setChat([...chat, message]);
        scrollToBottom();
      },
      error => {
        console.log("Message sending failed with error:", error);
      }
    );
    setMessage("");
  };
  const sendFile = () => {
    console.log("Send File Called")
    var mediaMessage = new CometChat.MediaMessage(
      selectedFriend,
      file,
      CometChat.MESSAGE_TYPE.FILE,
      CometChat.RECEIVER_TYPE.USER
    );
    CometChat.sendMediaMessage(mediaMessage).then(
      message => {
        console.log("file sent", message)
        setPlaceholder("")
        setChat([...chat, message]);
        scrollToBottom();
      },
      error => {
        return error
      }
    )
  }

Custom styles
Finally, the focus of this tutorial is building a functional chat app, hence I won’t explain the CSS in great detail so we don't deviate so much from the app. To apply the custom styling we've defined for the app open your App.css file and update it with the styles below:

/* App.css */
body {
  margin: 0;
  padding: 2rem 1.5rem;
  font: 1rem/1.5 "PT Sans", Arial, sans-serif;
  color: #5a5a5a;
}
.container {
  margin-top: 5%;
  margin-bottom: 5%;
}
.login-form {
  padding: 5%;
  box-shadow: 0 5px 8px 0 rgba(0, 0, 0, 0.2), 0 9px 26px 0 rgba(0, 0, 0, 0.19);
}   
.login-form h3 {
  text-align: center;
  color: #333;
}   
.login-container form {
  padding: 10%;
}
.message {
  overflow: hidden;
}
.balon1 {
  float: right;
  background: #35cce6;
  border-radius: 10px;
}   
.balon2 {
  float: left;
  background: #f4f7f9;
  border-radius: 10px; 
}
.img{
  width: 100%
}    
.login-form {
  padding: 5%;
  box-shadow: 0 5px 8px 0 rgba(0, 0, 0, 0.2), 0 9px 26px 0 rgba(0, 0, 0, 0.19);
}   
.login-form h3 {
  text-align: center;
  color: #333;
}  
.login-container form {
  padding: 10%;
}  
.message {
  overflow: hidden;
}
.col-lg-8 {
    flex: 0 0 100.666667%;
    max-width: 100%;
}
.row1 {
    display: flex;
    margin-right: -15px;
    margin-left: -15px;
}
.btn-file {
    position: relative;
    overflow: hidden;
    margin-top: 0.5em;
    margin-left: 0.5em
}
.btn-file input[type=file] {
    position: absolute;
    top: 0;
    right: 0;
    min-width: 100%;
    min-height: 100%;
    font-size: 100px;
    text-align: right;
    filter: alpha(opacity=0);
    opacity: 0;
    outline: none;   
    cursor: inherit;
    display: block;
}
.sfile{
  margin-left: 1em;
  margin-top: 0.5em
}

Alternatively, can also open the source code and copy the codes into your project.

Test the chat functionality
At this point, you can run the app. Open a new terminal window in the project's root directory and run the command below to start the development server:

npm start

Now when you navigate to your browser on localhost:3000 you should see the app running and the chat app functioning as expected.

Conclusion

This brings us to the end of this journey. Gradually we've been able to build a full chat application with realtime delivery and file sharing functionalities. This is only a part of all the things you can do with CometChat, you can take this tutorial further to add more features that are available on CometChat — Online presence indication, chat notifications, group chat functionalities, and more.