Build a chat app with a typing indicator using React.js

  • Christian Nwamba
November 2nd, 2018
You will need Node 5.2+ installed on your machine, and basic knowledge of React.

Chatkit is designed to make it simple for developers to add chat to web and mobile applications. It lets you add 1-1 and group chat to your app, along with typing indicators, file attachments and storage, user online presence and a flexible permissions system. In this post we’ll build a simple chat application with Chatkit and implement one of its amazing features - typing indicator.

Prerequisites

Before we start off building our chat app, there are a few things we should note:

  • You’ll need a basic knowledge of React (we’ll use the Create React App CLI)
  • You’ll need Node.js and NPM v5.2 or above
  • You’ll need access to Chatkit credentials, create an account to get it

That’s all.

Setting up Chatkit

Once you have successfully created an account and signed in to your dashboard, you’ll be required to create a Chatkit instance:

I called my instance chat-app, feel free to name yours as you please. Creating the new Chatkit instance will give you access to the credentials:

Keep the credentials handy as we’ll be using them soon.

Create a user

Once your instance is created, you’ll need to create a user. This will enable you interact with the instance we just created. Scroll down to the Console tab and create a new user:

This is a good way to manually create users and automatically add them to your chat room, however, we’ll be creating our users programmatically when they provide their details to enter our app.

Create a chat room

With Chatkit, all messages are sent to a room. You can have as many rooms as you want but for the scope of this tutorial, we’ll have just one room. Rooms can be created programmatically or from the Console tab. Here, we’ll use the Console tab and call it General. In the Console, click on the ROOMS tab and create room:

Note: A roomID will be generated when you create the chat room, be sure to note it as we’ll also use it in the next steps.

Enable test token provider

Chatkit is designed to integrate seamlessly with your existing authentication system. However, to help you get started quickly, Pusher provides a test token provider which you can enable in the TEST TOKEN PROVIDER tab:

Enable it by checking the checkbox. Once enabled, take note of your test token provider endpoint. You’ll need it in the next step.

Setup the chat app

Next let’s create the chat app. Like we mentioned earlier, we’ll use the Create React App tool to keep things simple and easy. Create a project folder and run the following command in it:

    npx create-react-app chat-app

Note: npx comes with npm 5.2+ and higher.

This takes a few minutes to set up your project folder. When it’s done simply change directory to the just created project folder and start the development server:

    // navigate to project folder
    $ cd chat-app

    // start the server
    $ npm start

When you’ve run this command, you’ll see this output in the terminal:

The project is now live at localhost:3000. If you navigate to it on your browser, you should see the default React homepage:

Set up the server

Now that we have the project created and ready, let’s setup a custom Node server and define a route that, when called, creates a Chatkit user dynamically for us. In the project root directory, open a terminal and run the following command:

    npm install --save cors express body-parser @pusher/chatkit-server

When the packages have been installed, create a new file server.js in the project directory and set it up like so:

    // server.js

    const express = require('express')
    const bodyParser = require('body-parser')
    const cors = require('cors')
    const Chatkit = require('@pusher/chatkit-server')
    const app = express()

    // init chatkit
    const chatkit = new Chatkit.default({
      instanceLocator: 'your-instance-locator',
      key: 'your-secret-key',
    })
    app.use(bodyParser.urlencoded({ extended: false }))
    app.use(bodyParser.json())
    app.use(cors())

    // create users
    app.post('/users', (req, res) => {
      const { username } = req.body
      console.log(username);
      chatkit
        .createUser({ 
        id: username, 
        name: username 
         })
        .then(() => res.sendStatus(201))
        .catch(error => {
          if (error.error_type === 'services/chatkit/user_already_exists') {
            res.sendStatus(200)
          } else {
            res.status(error.status).json(error)
          }
        })
    })
    const PORT = 3001
    app.listen(PORT, err => {
      if (err) {
        console.error(err)
      } else {
        console.log(`Running on port ${PORT}`)
      }
    })

Do well to replace the placeholders in the snippet above with your own unique keys before proceeding to start the server.

To start the server, open a terminal in the project folder and run:

    node server

Install Chatkit

Next, we install Chatkit into our application. Open another terminal window and run the following command to install Chatkit on the client side:

    npm install @pusher/chatkit-client --save

At this point, Chatkit is available in our application, let’s build!

Create users

We defined a /users route on our Node server to post supplied usernames and create Chatkit users. First, we need to ask users to supply their username once they open the app. To do that, let’s create a component to get the username. Create a new folder inside the src directory named components and create a new file GetUsername.js in it. Open the file and add this code to it:

    // src/components/GetUsername

    import React, { Component } from 'react'
    class GetUsername extends Component {
     constructor(props) {
       super(props)
       this.state = {
         username: '',
       }
       this.onSubmit = this.onSubmit.bind(this)
       this.onChange = this.onChange.bind(this)
     }
     onSubmit(e) {
       e.preventDefault()
       this.props.onSubmit(this.state.username)
     }
     onChange(e) {
        this.setState({ username: e.target.value })
      }
      render() {
        return (
          <div>
            <div>
              <h2>What is your username?</h2>
              <form onSubmit={this.onSubmit}>
                <input
                  type="text"
                  placeholder="Your full name"
                  onChange={this.onChange}
                />
                <input type="submit" />
              </form>
            </div>
          </div>
        )
      }
    }
     export default GetUsername;

When the username is supplied, we’ll make a POST request to the /users endpoint to create a user and return a username. To achieve this, open the App.js file and update it with the code below:

    // react-chat/src/App.js

    import React, { Component } from 'react'
    import GetUsername from './components/GetUsername'
    class App extends Component {
      constructor() {
        super()
        this.state = {
          currentUsername: '',
        }
        this.onUsernameSubmitted = this.onUsernameSubmitted.bind(this)
      }
      onUsernameSubmitted(username) {
        fetch('http://localhost:3001/users', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ username }),
        })
          .then(response => {
            this.setState({
              currentUsername: username
            })
          })
          .catch(error => console.error('error', error))
      }
        render() {
              return <GetUsername onSubmit={this.onUsernameSubmitted} />
            }

    }
    export default App;

If you run the app this point, you should get this on the browser:

Chat component

When the user supplies the username, we want to transition them into the Chat component where they can actually start sending messages. In the components folder, create a new file Chat.js and update it with the code below:

    // src/components/Chat.js

    import React, { Component } from 'react'

    class Chat extends Component {  
      render() {
        return (
          <div>
            <h1>This is the Chat component</h1>
          </div>
        )
      }
    }

    export default Chat

Let’s update App.js with this new component and set it up such we render this component when the user have successfully provided their username. Open App.js and update it like so:

    // react-chat/src/App.js

    import React, { Component } from 'react'
    import GetUsername from './components/GetUsername';
    import Chat from './components/Chat'
    class App extends Component {
      constructor() {
        super()
        this.state = {
          currentUsername: '',
          visibleScreen: "getUsernameScreen"
        }
        this.onUsernameSubmitted = this.onUsernameSubmitted.bind(this)
     }
      onUsernameSubmitted(username) {
        fetch('http://localhost:3001/users', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ username }),
        })
          .then(response => {
            this.setState({
              currentUsername: username,
             visibleScreen: 'Chat'
            })
          })
          .catch(error => console.error('error', error))
      }
     render() {
        if (this.state.visibleScreen === 'getUsernameScreen') {
          return <GetUsername onSubmit={this.onUsernameSubmitted} />
        }
        if (this.state.visibleScreen === 'Chat') {
          return <Chat currentUsername={this.state.currentUsername} />
        }
      }
    }
    export default App

Now when you open the app on the browser and provide a username, the app renders the chat component:

Connect to Chatkit

Now that we have our Chat component, let’s go ahead and start building off the chat functionality of the app. First, we’ll connect to our Chatkit instance using the credentials we generated on the dashboard. Again, open Chat.js and update it with the code below:

    // src/components/Chat.js

    import React, { Component } from 'react'
    import '../App.css';
    import Chatkit from '@pusher/chatkit-client';
    import Message from './Message';

    class Chat extends Component {  
        constructor(props){
              super(props);
              this.state ={
                messages:[],
                currentRoom: {},
                currentUser: {},
                chatInput: ''
              }        
            this.setChatInput = this.setChatInput.bind(this);
            this.sendMessage = this.sendMessage.bind(this);
            this._handleKeyPress = this._handleKeyPress.bind(this);
            }      
    // update the input field when the user types something
        setChatInput(event){
            this.setState({
                chatInput: event.target.value
            });
          }      
        sendMessage() {
                 if(this.state.chatInput){
                    this.state.currentUser.sendMessage({
                        text: this.state.chatInput,
                        roomId: this.state.currentRoom.id,
                      })
                    }     
                    this.setState({ chatInput: ''})
                 }       
        _handleKeyPress(e){
                            if (e.key === 'Enter') {
                                this.sendMessage();
                            }
                        }      
        componentDidMount() {
              const chatManager = new Chatkit.ChatManager({
                      instanceLocator: 'your-instance-locator',
                      userId: this.props.currentUsername,
                      tokenProvider: new Chatkit.TokenProvider({
                        url: 'your-token-provider',
                      }),
                    })
                    chatManager
                    .connect()
                    .then(currentUser => {
                      this.setState({ currentUser })
                      return currentUser.subscribeToRoom({
                        roomId: 'your-room-id',
                        messageLimit: 100,
                        hooks: {
                          onMessage: message => {
                            let newMessages = this.state.messages;           
                            newMessages.push(<Message 
                                                        key={ 
                                                            this.state.messages.length 
                                                        } 
                                                        senderId={ 
                                                            message.senderId 
                                                        } 
                                                        text={ message.text 
                                                        }/>)         
                            this.setState({messages: newMessages})
                          },
                        },
                      })
                    })      
                    .then(currentRoom => {
                      this.setState({ currentRoom })
                     })
                    .catch(error => console.error('error', error))
            }       
            render() {
                        return ( 
                            <div id="center">
                                <div id="chat-output">
                                { this.state.messages }     
                                </div>                           
                                <input id="chat-input"
                                    type="text"
                                    placeholder='Type message...'
                                    name=""
                                    value={ this.state.chatInput } 
                                    onChange={ this.setChatInput } 
                                    onKeyPress={ this._handleKeyPress }/>                 
                                <div id="btndiv">
                                <input id="button" type="button"
                                    onClick={ this.sendMessage } value="Send Chat" />
                                </div>                           
                            </div>
                        );
                    }      
    }
    export default Chat

That’s a mouth full. Let’s step through the code together. First we initialized the application state with the necessary objects since we’ll be subscribing to chatrooms, sending messages in it, keeping track of current users and monitoring their text inputs.

Next we collect the text typed by the user and send the message in our sendMessage() function after which we then clear the input. In the componentDidMount() lifecycle method, we initialized our Chatkit instance with the credentials we got on registration, subscribed to our chat room and connected to our Chatkit instance.

Then on receiving a new message in the chat room, we render it on screen using the Message component which we are yet to create at this point. Finally in the render() function, we set up a simple div to display the messages, an input to collect the text and a button to send the message. Do not run the code yet till we set up the Message component.

Message component

To help us render the sent messages on screen, we’ll create a Message component in the components folder and update it with this code:

    // src/components/Message.js

    import React, { Component } from 'react'
    class Message extends Component{
        render () {
            return ( 
                <div> 
                { this.props.senderId }: { this.props.text } 
                </div>
            );
        }
    };
    export default Message;

Note: replace the highlighted values with your proper credentials from your Chatkit dashboard.

Here we are simply passing the returned data via props so that we can render it on screen. before we run the app, lets brush up the UI a bit.

Update the UI

Open the App.css file and update it with the code below:

    // src/App.css

    #chat-input {
      color: 'inherit';
      background-color: 'none';
      outline: 'none';
      border: 'none';
      flex: 1;
      font-size: 16;
      padding: 20px;
      margin-top: 20px;
    }
    #container{
      padding: 20;
      border-top: '1px #4C758F solid';
      margin-bottom: 20;
      margin-top: 150px;
    }
    #btndiv{
      color: 'inherit';
      background-color: 'none';
      outline: 'none';
      border: 'none';
      flex: 1;
      font-size: 16;
      padding: 20px;
    }
    #button{
      color: #73AD21;
      padding-right: 20px;
      font-size: 100px;
      margin-bottom: 20px;
    }
    #center {
      margin: auto;
      width: 60%;
      border: 3px solid #73AD21;
      padding: 10px;
    }

Now if you save all files and check back on the browser, you should be able to see this output now:

Wonderful. Now our chat functionality works!! If you open a second tab, and join the chat as a different user, you’ll be able to share messages.

Typing indicator

Now that we have our chat features set, let’s implement the typing indicator. Chatkit has the functionality that allows us to determine which user is currently typing. Here, we’ll implement it this application such that when one user is typing, the rest will see a message saying that that user is typing. The message will equally disappear when the user stops typing.

To get started with the typing indicator, create a new file TypingIndicator.js inside the src/components directory and update it with this code:

    // src/components/TypingIndicator

    import React, { Component } from 'react'
    class TypingIndicator extends Component {
      render() {
        if (this.props.typingUsers.length > 0) {
          return (
            <div>
              {`${this.props.typingUsers
                .slice(0, 2)
                .join(' and ')} is typing`}
            </div>
          )
        }
        return <div />
      }
    }
    export default TypingIndicator

Now that we have this component, let’s update our Chat.js file to show the indication when users starts typing.

    // src/components/Chat.js

    import React, { Component } from 'react'
    import '../App.css';
    import Chatkit from '@pusher/chatkit-client';
    import Message from './Message';
    import TypingIndicator from './TypingIndicator';

    class Chat extends Component {  
        constructor(props){
              super(props);
              this.state ={
                messages:[],
                currentRoom: {},
                currentUser: {},
                typingUsers: [],
                chatInput: '',
                }
            this.sendMessage = this.sendMessage.bind(this);
            this._handleKeyPress = this._handleKeyPress.bind(this);
            this.sendTypingEvent = this.sendTypingEvent.bind(this);
            }
            sendMessage() {
                     if(this.state.chatInput){
                        this.state.currentUser.sendMessage({
                            text: this.state.chatInput,
                            roomId: this.state.currentRoom.id,
                          })
                        }
                this.setState({ chatInput: ''})
            }

    // Send typing event
      sendTypingEvent(event) {
            this.state.currentUser
              .isTypingIn({ roomId: this.state.currentRoom.id })
              .catch(error => console.error('error', error))
              this.setState({
                chatInput: event.target.value
            });
          }

        _handleKeyPress(e){
                  if (e.key === 'Enter') {
                      this.sendMessage();
                  }
              }   
        componentDidMount() {
              const chatManager = new Chatkit.ChatManager({
                      instanceLocator: 'your-instance-locator',
                      userId: this.props.currentUsername,
                      tokenProvider: new Chatkit.TokenProvider({
                        url: 'your-provider-token',
                      }),
                    })
                    chatManager
                    .connect()
                    .then(currentUser => {
                      this.setState({ currentUser })
                      return currentUser.subscribeToRoom({
                        roomId: your-room-id,
                        messageLimit: 2,
                        hooks: {
                          onNewMessage: message => {                      
                            let newmessage = this.state.messages;           
                            newmessage.push(<Message 
                                              key={ 
                                                  this.state.messages.length 
                                              } 
                                              senderId={ 
                                                  message.senderId 
                                              } 
                                              text={ message.text 
                                              }/>)

                            this.setState({messages: newmessage})
                          },
                        onUserStartedTyping: user => {
                                this.setState({
                                  typingUsers: [...this.state.typingUsers, user.name],
                                })
                              },
                        onUserStoppedTyping: user => {
                                this.setState({
                                  typingUsers: this.state.typingUsers.filter(
                                    username => username !== user.name
                                  ),
                                })
                              },
                        },
                      })
                    })
                    .then(currentRoom => {
                      this.setState({ currentRoom })
                     })
                    .catch(error => console.error('error', error))
            }

            render() {
                        return ( 
                            <div id="center">
                                <div id="chat-output">
                                { this.state.messages }     
                                </div> 
                                <input id="chat-input"
                                    type="text"
                                    placeholder='Type message...'
                                    name=""
                                    value={ this.state.chatInput } 
                                    onChange={ this.sendTypingEvent } 
                                    onKeyPress={ this._handleKeyPress }/>                 
                                <div id="btndiv">
                                <input id="button" type="button"
                                    onClick={ this.sendMessage } value="Send Chat" />
                                <TypingIndicator typingUsers={this.state.typingUsers} />
                                </div>
                            </div>
                        );
                    }

    }
    export default Chat

Here we’ve only done a few things, first we imported the TypingIndicator component. Then we initialized typingUsers in the state object so that we can track users who are typing. Next, we created a new sendTypingEvent() to track the particular user who is typing in the chat room.

When using Chatkit, typing indicators boil down to two fundamental actions:

  • Calling currentUser.userIsTyping when the current user starts typing; then,
  • Listening to userStartedTyping and userStoppedTyping events.

That is what we’ve also done in the componentDidMount() lifecycle method. Then in the render() function, we also rendered the TypingIndicator component so we can display it on screen when users start typing.

When you hit save and reload the browser, you should be able to see the typing indicator on a second tab when typing on the first:

That’s it! We have now built a fully functional React chat application with typing indicators using Pusher Chatkit.

Conclusion

Chatkit is an amazing tool for adding chat functionalities to your web and mobile applications. Here, we have successfully demonstrated how to build a React chat application with Chatkit. We also implemented the Chatkit typing indicator functionality however, we haven’t scratched the surface of what it can do. To get more out of Chatkit and look at what more it has to offer, feel free to visit the documentation page. We have also provided this project for you on GitHub if you want to quickly access the source code for reference.

  • Chatkit

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.