Build a chat app with React and React Bootstrap

Last updated
July 19, 2019
by
Idorenyin, React developer from Nigeria 🇳🇬Follow me on Twitter @kingidee

Idorenyin Obong

Allowing users to communicate is becoming an essential feature for many apps. In my experience, chat closes the distance between you and your customers and can lead to more conversions, improved engagement and ultimately, greater success for your business.

Build a chat app with React and React Bootstrap

Table of Contents

    Allowing users to communicate is becoming an essential feature for many apps. In my experience, chat closes the distance between you and your customers and can lead to more conversions, improved engagement and ultimately, greater success for your business. However, implementing chat can be time-consuming.

    In this tutorial, I am excited to show you how you can build an aesthetic group chat with minimal code by leveraging, React, React Bootstrap, and CometChat.

    Here is a preview of what you will build:

    You can choose to dive right into code or go through our step by step tutorial.

    Scaffolding a new React project

    For this article, to quickly scaffold a new React app, you will use one of the very popular tools available - the create-react-app CLI tool. Open a terminal, move into the directory where you usually save your projects and run this command:

    npx create-react-app react-anonymous-chat

    After running the command, the CLI will begin the process of installing the default dependencies for a React project. Depending on your internet speed, this should take a couple of minutes to complete. After setting up your project, open your new project in your preferred text editor or IDE.

    Installing Dependencies

    Now that you have scaffolded your app, the next step is to install dependencies necessary for your chat application. For this article, you’ll need the following:

    • @cometchat-pro/chat: This module will enable us to connect to CometChat and begin sending and receiving messages in real-time
    • react-bootstrap: This is a UI library built on top of react and core Bootstrap. You will use it to style the entire app in this article
    • react-router-dom: You will use it for client-side routing
    • uuid: This module will be used to generate unique identifiers

    To install the above modules, run the following commands:

    # move into your project directory
    cd react-anonymous-chat

    # install dependencies using npm
    npm install @cometchat-pro/chat react-bootstrap react-router-dom uuid

    Setting up

    To begin using the CometChat Pro SDK in your newly created React project, you’ll need a CometChat Pro account. If you don’t have an account, you can quickly create one here.

    After creating an account, go to your dashboard and create a new app called react-anonymous-chat.

    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.

    After creating a new app, you will find the APP ID attached close to the name of your app. If you open your app and go to the API Keys section, you will see a key with fullAccess scope. Copy it out as well as the APP ID. We’ll need these shortly.

    Next, create a .env file in the root of your project to store your app credentials. Take care not to commit this file to version control! This is important for protecting your secrets when you publish your app. You can easily create the file by running this command:

    touch .env

    Open the file and paste this snippet:

    {% c-line %}REACT_APP_COMETCHAT_APIKEY=YOUR_API_KEY_GOES_HERE{% c-line-end %}
    {% c-line %}REACT_APP_COMETCHAT_APPID=YOUR_APP_ID_GOES_HERE{% c-line-end %}

    Replace the placeholders with your APP ID and API KEY from your dashboard.

    Since your keys are now ready, you can initialize CometChat in the index.js file generated by Create React App. Open your index.js file and replace it with this snippet:

    {% c-block language=“javascript” %}
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import { CometChat } from '@cometchat-pro/chat';
    CometChat.init(process.env.REACT_APP_COMETCHAT_APPID)
    .then(() => {
    console.log('Initialised CometChat');
    })
    .catch(() => {
    console.log('Failed to Initialise CometChat');
    });
    ReactDOM.render(, document.getElementById('root'));
    {% c-block-end %}

    Before going ahead, you’ll need to import Bootstrap in public/index.htm like so:

    {% c-block %}
    <link
    rel="stylesheet"
    href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
    crossorigin="anonymous"
    />
    {% c-block-end %}

    Building your components

    Your app will have three components, the signup, home, and chat component. The signup component is the page that will enable users to create new accounts. Create a folder named components inside the src directory. This is where you will add your components.

    Signup component

    In this component, you will build out a form to help create new users on the app. A user will have a UID, an email address, and a name. The UID value has to be unique.

    Create a new file named Signup.js, Inside the file, add these imports:

    {% c-block language=“javascript” %}
    import React from 'react';
    import Button from 'react-bootstrap/Button'
    import Row from 'react-bootstrap/Row'
    import Col from 'react-bootstrap/Col'
    import Form from 'react-bootstrap/Form'
    import Alert from 'react-bootstrap/Alert'
    import Spinner from 'react-bootstrap/Spinner'
    import { Redirect, Link } from 'react-router-dom'
    {% c-block-end %}

    Here, you are importing some components from the core react-bootstrap components as well as components from react-router-dom dependency.

    Next, you define the initial state for your signup component in the Signup.js file:

    {% c-block language=“javascript” %}
    class Signup extends React.Component {
    constructor(props) {
    super(props);
    this.state = {
    uid: '',
    name: '',
    email: '',
    UIDError: null,
    errors: null,
    redirect: false,
    isLoading: false
    };
    }
    //... other class methods
    }
    export default Signup;rel="stylesheet"
    href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
    crossorigin="anonymous"
    />
    {% c-block-end %}


    Here, you defined a state to hold data for the signup form and error messages. Here are the specific functions of each of the objects declared in the state:

    • uid: This holds the current value of the text inputted in the username form field.
    • name: This holds the current value of the user’s name in the form field.
    • email: This holds the current value of the user’s email in the form field.
    • UIDError: This object will keep track of errors when validating the username field.
    • errors: This stores error messages when validating other fields.
    • redirect: This keeps tracks of success on form submission.
    • isLoading: This is used for providing visual feedback when using <Spinner /> component.

    The UIDError object keeps track of errors on the username field while errors keeps track of errors on other fields. They are separated because the username field does not accept spaces and as such, they do not have the same validation logic.

    After defining the state, you will create the user interface to represent the current state of your application. Add this render method to your Signup class:

    {% c-block language=“javascript” %}
    render() {
     if (this.state.redirect) return ;
     return (
    <React.Fragment>
    <Row
           className='d-flex justify-content-center align-items-center w-100 mt-5'
           style={{
             minHeight: '100%'
           }}
    >
    >Col>
           {this.state.errors !== null && (
    <Alert variant='danger'>
    <ul>
                 {this.showErrors().map(err => (
    <li key={err}>{err</li>
                 ))}
    </ul>
    </Alert>
           )}
    <Form onSubmit={this.handleSubmit}>
    <Form.Group controlId='username'>
    <Form.Label>User ID</Form.Label>
    <Form.Control
                 required
                 type='text'
                 name='uid'
                 value={this.state.uid}
                 placeholder='Choose a username'
                 onChange={this.handleChange}
               />
               {this.state.UIDError !== null && (
    <Form.Control.Feedback
                   style={{ display: 'block' }}
                   type='invalid'
    >
                   {this.state.UIDError}
    </Form.Control.Feedback>
               )}
    </Form.Group>
    <Form.Group controlId='display-name'>
    <Form.Label>Name</Form.Label>
    <Form.Control
                     required
                     type='text'
                     name='name'
                     value={this.state.name}
                     placeholder='What is your name?'
                     onChange={this.handleChange}
                   />
    </Form.Group>
    <Form.Group controlId='email'>
    <Form.Label>Email Address</Form.Label>
    <Form.Control
                     required
                     type='email'
                     name='email'
                     value={this.state.email}
                     placeholder='Your email address'
                     onChange={this.handleChange}
                   />
    </Form.Group>
    <Button
                   disabled={this.state.isLoading}
                   variant='primary'
                   type='submit'
                   className='btn-block'
    >
                   {this.state.isLoading ? (
    <>
    <Spinner
                         as='span'
                         animation='grow'
                         size='sm'
                         role='status'
                         aria-hidden='true'
                       />
                       Please wait...
    </>
                   ) : (
    <span>Create My Account</span>
                   )}
    </Button>
    <p className='pt-3'>
                   Already have an account? <Link to='/'>Login</Link>
    </p>
    </Form>
    </Col>
    </Row>
    </React.Fragment>
       );
     }
    {% c-block-end %}

    Here in this snippet, you declared a form where the values of the inputs are bound to the state you defined earlier. The form contains three inputs with native form validation except for the username input. It also contains a <Redirect /> component and a Link that renders the home component where necessary.

    Next, you will create three methods used in the render method, namely: handleChange, handleSubmit and showErrors. Add these methods to the your Signup.js file

    {% c-block language=“javascript” %}
    handleChange = e => {
     if (e.target.name === 'uid') {
       const uid = e.target.value;
       if (uid.indexOf(' ') > 0) {
         this.setState(
           { UIDError: 'Username cannot contain white spaces' },
           () => {
             console.log(this.state.UIDError);
           }
         );
       } else {
         this.setState({ UIDError: null });
       }
     }
     this.setState({ [e.target.name]: e.target.value });
    };
    {% c-block-end %}

    {% c-block language=“javascript” %}
    handleSubmit = e => {
     e.preventDefault();
     const { uid, name, email } = this.state;
     this.setState({ uid: '', name: '', email: '', isLoading: true });
     fetch('https://api.cometchat.com/v1/users', {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
         appid: process.env.REACT_APP_COMETCHAT_APPID,
         apikey: process.env.REACT_APP_COMETCHAT_APIKEY
       },
       body: JSON.stringify({
         uid,
         name,
         email
       })
     })
     .then(response => response.json())
     .then(data => {
       const error = data.error;
       if (error) {
         this.setState(
           {
             isLoading: false,
             errors: { ...error.details }
           },
           () => {
             this.showErrors();
           }
         );
         return;
       }
       this.setState({
         isLoading: false,
         redirect: true
       });
     });
    };
    {% c-block-end %}

    {% c-block language=“javascript” %}
    showErrors = () => {
     const errors = this.state.errors;
     let errorMessages = [];
     if (errors !== null) {
       for (const error in errors) {
         errorMessages = [...errorMessages, ...errors[error]];
       }
     }
     return errorMessages;
    };
    {% c-block-end %}

    If you are building a production app, it is not proper to keep your keys on the frontend. Instead, the keys should be kept on the server-side so that the private key can remain private.

    The handleChange method updates the values of all the input fields as the user types. A custom validation is performed on the username field to prevent usernames without white spaces. The handleSubmit() method makes a POST request to the account creation API: https://api.cometchat.com/v1/users with the details entered by the user. If it is successful, you are then redirected to the home page. The showErrors method is used to show errors.

    Home component

    Now that you are done with the signup component, you will now build the home component. This component is to enable logging in of users.

    Create a new file Home.js inside the /src/components directory. Inside the file, add these imports:

    {% c-block language=“javascript” %}
    import React from 'react';
    import Button from 'react-bootstrap/Button';
    import Row from 'react-bootstrap/Row';
    import Col from 'react-bootstrap/Col';
    import Form from 'react-bootstrap/Form';
    import Alert from 'react-bootstrap/Alert';
    import Spinner from 'react-bootstrap/Spinner';
    import { CometChat } from '@cometchat-pro/chat';
    import { Redirect, Link } from 'react-router-dom';
    {% c-block-end %}

    Here, you imported components that you will make use of just like you did in the signup component. After that, add this snippet in the class:

    {% c-block language=“javascript” %}
    class Home extends React.Component {
     constructor(props) {
       super(props);
       this.state = {
         username: '',
         user: null,
         error: null,
         redirect: false,
         isLoading: false
       };
     }
     //... other class methods
    }
    export default Home;
    {% c-block-end %}

    Here, you declared the initial state for this component. This is similar to what you did in the signup component also, except that you have a username and user object to hold data about the logged in user.

    After that, add these two methods to your class handleChange and handleSubmit like so:

    {% c-block language=“javascript” %}
    handleChange = e => {
     this.setState({ username: e.target.value });
    };
    {% c-block-end %}

    {% c-block language=“javascript” %}
    handleSubmit = e => {
     e.preventDefault();
     const username = this.state.username;
     this.setState({ username: '', isLoading: true });
     CometChat.login(username, process.env.REACT_APP_COMETCHAT_APIKEY)
     .then(user => {
       this.setState({ redirect: true, user, isLoading: false });
       localStorage.setItem('cometchat:authToken', user.authToken);
     })
     .catch(err => {
       this.setState({ error: err.message, isLoading: false });
     });
    };
    {% c-block-end %}

    The handleChange method updates the value of the input field as the user types while the handleSubmit method will call the login method provided by CometChat. To make a login request, the API key defined in the .env file is passed alongside the username.

    On successful login, the user data is returned and the authToken is saved for re-authentication later. Next, add the render method for this component below the handleSubmit method like so:

    {% c-block language=“javascript” %}
    // other methods above...
    render() {
     if (this.state.redirect)
     return (
    <Redirect
         to={{
           pathname: '/chat',
           user: this.state.user
         }}
       />
     );
     return (
    <React.Fragment>
    <Row
           className='d-flex justify-content-center align-items-center w-100 mt-5'
           style={{
             minHeight: '100%'
           }}
    >
    <Col xs={10} sm={10} md={4} lg={4} className='mx-auto mt-5'>
             {this.state.error !== null && (
    <Alert variant='danger'>{this.state.error}</Alert>
             )}
    <Form onSubmit={this.handleSubmit}>
    <Form.Group controlId='username'>
    <Form.Label>Username</Form.Label>
    <Form.Control
                   required
                   type='text'
                   value={this.state.username}
                   placeholder='Enter a Username'
                   onChange={this.handleChange}
                 />
    </Form.Group>
    <Button
                 disabled={this.state.isLoading}
                 variant='primary'
                 type='submit'
                 className='btn-block'
    >
                 {this.state.isLoading ? (
    <>
    <Spinner
                       as='span'
                       animation='grow'
                       size='sm'
                       role='status'
                       aria-hidden='true'
                     />
                     Loading...
    </>
                 ) : (
    <span>Login</span>
                 )}
    </Button>
    <p className='pt-3'>
                 Don't have an account? <Link to='/signup'>Create One</Link>
    </p>
    </Form>
    </Col>
    </Row>
    </React.Fragment>
     );
    }
    {% c-block-end %}

    In this snippet, you have a login form to take the user’s username. When the user clicks on the Login button, you take the user input and call the handleSubmit method you defined earlier in this component. If a success response is received, the user is redirected to the chat component, else, an error is displayed.

    Chat component

    This is the component where a user will be able to view messages and send messages in a chat group. First, create a new Chat.js file in the src/components directory. After that, add these imports:

    {% c-block language=“javascript” %}
    import React from 'react';
    import { CometChat } from '@cometchat-pro/chat';
    import Row from 'react-bootstrap/Row';
    import Col from 'react-bootstrap/Col';
    import Container from 'react-bootstrap/Container';
    import Form from 'react-bootstrap/Form';
    import Button from 'react-bootstrap/Button';
    import Navbar from 'react-bootstrap/Navbar';
    import { Redirect } from 'react-router-dom';
    import uuid from 'uuid';
    {% c-block-end %}

    After that, add a class with a state inside the Chat.js file like so:

    {% c-block language=“javascript” %}
    class Chat extends React.Component {
     constructor(props) {
       super(props);
       this.state = {
         redirect: false,
         user: null,
         receiverID: 'supergroup',
         messageText: '',
         messages: [],
         authToken: null,
         messageType: CometChat.MESSAGE_TYPE.TEXT,
         receiverType: CometChat.RECEIVER_TYPE.GROUP
       };
     }
     //... other class methods
    }
    {% c-block-end %}


    export default Chat;

    Here, you need a messages array to store all messages sent and received on the group. The messageType and receiverType objects define the type of message you want to listen for and for whom the message is for. The receiverID object is used to identify the group name on which you listen for messages. Here, you used the default group generated for you - supergroup.

    After that, add the render method for the component just below the constructor like this:

    {% c-block language=“javascript” %}
    render() {
     if (this.state.redirect) return <Redirect to='/' />;
     return (
    <div
         className='bg-light page'
         style={{ height: '100vh', overflowX: 'hidden' }}
    >
    <Row>
    <Col>
    <Container>
    <div className='d-flex align-items-center justify-content-between'>
    <h3 className='text-center py-3 d-inline'>
                   React Anonymous Chat
    </h3>
    <Button onClick={e => this.logout()} variant='outline-primary'>
                   Logout
    </Button>
    </div>
    <ul className='list-group' style={{ marginBottom: '60px' }}>
                 {this.state.messages.length > 0 ? (
                   this.state.messages.map(msg => (
    <li className='list-group-item' key={uuid()}>
    <strong>{msg.sender.name}</strong>
    <p>{msg.text}</p>
    </li>
                   ))
                 ) : (
    <div className='text-center mt-5 pt-5'>
    <p className='lead text-center'>Fetching Messages</p>
    </div>
                 )}
    </ul>
    </Container>
    </Col>
    </Row>
    <Navbar fixed='bottom'>
    <Container>
    <Form
               inline
               className='w-100 d-flex justify-content-between align-items-center'
               onSubmit={this.sendMessage}
    >
    <Form.Group style={{ flex: 1 }}>
    <Form.Control
                   value={this.state.messageText}
                   style={{ width: '100%' }}
                   required
                   type='text'
                   placeholder='Type Message here...'
                   onChange={this.handleChange}
                 />
    </Form.Group>
    <Button variant='primary' type='submit'>
                 Send
    </Button>
    </Form>
    </Container>
    </Navbar>
    </div>
     );
    }
    {% c-block-end %}

    In this render method, you have a <Redirect /> component that redirects to the home component when there is no logged in user. You also have a message box that displays all messages sent and received in the group, and finally, you have a form to handle the sending of messages.

    There are some methods that are called here, don’t worry yet, you will soon define these methods. Now that you have built the UI for the chat component, the next thing is to show messages to the user. You will do this as soon as the component is mounted. In your Chat.js file, add this method:

    {% c-block language=“javascript” %}
    componentDidMount() {
     this.setState({ user: this.props.location.user });
     this.getUser();
     this.receiveMessages();
    }
    {% c-block-end %}

    This is a callback function provided by React. In this method, you will fetch the user details and listen for new messages in the group. Now, add the getUser() method like so:

    {% c-block language=“javascript” %}
    getUser = () => {
     CometChat.getLoggedinUser().then(
       user => {
         this.joinGroup();
       },
       error => {
         const authToken = localStorage.getItem('cometchat:authToken');
         if (authToken !== null) {
           this.setState({ authToken }, () => {
             this.reAuthenticateUserWithToken(this.state.authToken);
           });
         } else {
           this.setState({ redirect: true });
         }
       }
     );
    };
    {% c-block-end %}

    In this method, you get the logged in user and join the group using the joinGroup() method . If an error occurs in getting the user, the authToken stored in localStorage serves as a fallback option for re-authenticating the user. The joinGroup() method is not defined yet. Create the method inside your Chat.js to look like this:

    {% c-block language=“javascript” %}
    joinGroup = () => {
     const GUID = this.state.receiverID;
     const password = '';
     const groupType = CometChat.GROUP_TYPE.PUBLIC;
     CometChat.joinGroup(GUID, groupType, password).then(
       group => {},
       error => {
         if (error.code === 'ERR_ALREADY_JOINED') {
           this.reAuthenticateUserWithToken();
         }
       }
     );
    };
    {% c-block-end %}

    Here in this method, the user is subscribed to this group and they can now send and receive messages from this group. Also, the fetchMessages() method is called to fetch previous messages when the user successfully joins the group. Add the fetchMessages() method too:

    {% c-block language=“javascript” %}
    fetchMessages = () => {
     const GUID = this.state.receiverID;
     const limit = 30;
     const messagesRequest = new CometChat.MessagesRequestBuilder()
       .setGUID(GUID)
       .setLimit(limit)
       .build();
     messagesRequest.fetchPrevious().then(
       messages => {
         const textMessages = messages.filter(msg => msg.type === 'text');
         this.setState({ messages: [...textMessages] });
         this.scrollToBottom();
       },
       error => {
         console.log('Message fetching failed with error:', error);
       }
     );
    };
    {% c-block-end %}

    This fetches the previous messages sent to the group. To enable users to see the newest messages, the scrollToBottom() method is called. Add a scrollToBottom() method to your class like so:

    {% c-block language=“javascript” %}
    scrollToBottom = () => {
     const page = document.querySelector('.page');
     page.scrollTop = page.scrollHeight;
    };
    {% c-block-end %}

    Now that you can fetch previous messages, it’s time to enable users to send new messages too. To achieve this, you first need to create a handleChange() method to update the state whenever the user types a new message. Add this method to your class component:

    {% c-block language=“javascript” %}
    handleChange = e => {
     this.setState({ messageText: e.target.value });
    };
    {% c-block-end %}

    Thereafter, you add the sendMessage method like so:

    {% c-block language=“javascript” %}
    sendMessage = e => {
     e.preventDefault();
     const { receiverID, messageText, messageType, receiverType } = this.state;
     const textMessage = new CometChat.TextMessage(
       receiverID,
       messageText,
       messageType,
       receiverType
     );
     CometChat.sendMessage(textMessage).then(
       message => {
         this.setState({ messageText: '' });
         const oldMessages = [...this.state.messages];
         const filtered = oldMessages.filter(msg => msg.id !== message);
         this.setState({ messages: [...filtered, message] });
         this.scrollToBottom();
       },
       error => {
         console.log('Message sending failed with error:', error);
       }
     );
    };
    {% c-block-end %}

    This method is called when the form in the render() method is submitted. After the sendMessage method of ComeChat is called, the input field is cleared and new messages will be added to the messages array. New messages are also filtered in case of duplicates, and lastly, the scrollToBottom() is called to give focus to new messages.

    The second method you called in the componentDidMount method was receiveMessages. Now, create the method inside your class:

    {% c-block language=“javascript” %}
    receiveMessages = () => {
     const listenerID = 'supergroup';
     CometChat.addMessageListener(
       listenerID,
       new CometChat.MessageListener({
         onTextMessageReceived: textMessage => {
           const oldMessages = this.state.messages;
           oldMessages.push(textMessage);
           this.setState(
             {
               messages: [...oldMessages]
             },
             () => this.scrollToBottom()
           );
         }
       })
     );
    };
    {% c-block-end %}

    Since it’s only text messages that you are concerned with, only the onTextMessageReceived handler is used. On receiving new messages, the messages array is updated to show messages in real time.

    After that, you have to add a logout method to enable authenticated users to log out of the application. Add a logout method in the Chat.js file like so:

    {% c-block language=“javascript” %}
    logout = () => {
     CometChat.logout().then(() => {
       localStorage.removeItem('cometchat:authToken');
       this.setState({ redirect: true });
     });
    };
    {% c-block-end %}

    When a user clicks the logout button, you call the logout() method, then, you reset the localStorage and redirect the user to the Home page.

    Now that you have defined your components, you would update the App.js file with the routes. Open your App.js file and replace it with this:

    {% c-block language=“javascript” %}
    import React from "react";
    import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
    import Home from "./components/Home";
    import Chat from "./components/Chat";
    import Signup from "./components/Signup";
    function App() {
     return (
    <Router>
    <Switch>
    <Route exact path="/" component={Home} />
    <Route exact path="/signup" component={Signup} />
    </Switch>
    </Router>
     );
    }
    export default App;
    {% c-block-end %}

    Now, you have successfully finished building your app. Run this command in the root directory of your app:

    npm start

    You should have something similar to what was shown to you earlier.

    Conclusion

    In this article, you learned how to build an anonymous chat using React, React Bootstrap and CometChat Pro. You can now comfortably integrate group chats into React apps. As far as CometChat Pro SDK goes, there are a ton of other features not covered in this article. Feel free to expand on this by diving deeper into the documentation.

    What to Read Next

    No items found.