Build Chat Apps with React, Typescript, and Components

9 min readDec 18, 2020

Modular chat kits, powered by PubNub, make it easy to build the type of chat you need for your application. They include a framework for sending and receiving messages, group and 1-to-1 communication, Presence and activity status, and a path forward for expanding with more features and functionality. These open source kits can be used to build chat rooms for applications like live events, HIPAA compliant doctor patient messaging, and more. 

This tutorial will cover how to build a simple chat application using modern component-based design in React. React offers an easy way to group UI elements into components. A complete chat application can go from having dozens of UI objects to just a few UI objects using components. Once you use chat components, you’ll have a flexible chat solution that can be reused anywhere easily.

Each component is designed to be reusable, expandable, and easy to use in another React application. Simply edit the CSS and layout of the open source React components to your liking. Anyone can build a robust chat app by using this tutorial and these components as a starting point.

Simple Chat
Simple Chat

What backend API should you use for chat apps?

Before we jump in to the rest of the tutorial, it’s a good idea to figure out what will power the backend of our chat app. This tutorial is an example of how to integrate open source React chat components, with PubNub powering the backend as the chat API. We’ll use PubNub for reliable message deliverability, presence indicators, and other chat-related data so that the React-based front end components can update in real time as users send and receive messages.

PubNub is a great backend API for chat because it offers a lot of functionality. Some key advantages are:

  • PubNub already delivers billions of messages every day. You can trust it’ll scale with your application.  

  • Storage and Playback for enabling richer chat applications. Message Persistence is essential for a complete user experience.

  • Presence is built-in. You can easily create online and offline indicators. 

  • Mobile Push Notifications are supported. Instantly notify users of new messages on iOS and Android apps.

Using these React components, you don’t need to be previously familiar with PubNub or PubNub SDKs. PubNub is already fully integrated with the components covered here and this tutorial covers how Pubnub works for each chat component. It is, however, recommended that you’re familiar with Typescript and React if you wish to modify these React components for your own use in your application.

Getting started with React and Typescript

Now that we know what backend we are going to use, let's review React and Typescript. React is the most used framework used by developers to develop web apps. TypeScript is a type safe language built on top of Javascript. TypeScript is now the new standard in the React world as it offers more functionality over Javascript.

You can still follow this tutorial if you’re not familiar with React or Typescript. We’ll provide all the steps you need here to get started with these open source React chat app components.

The React docs provide a few great ways to get started learning React that are flexible with various common learning styles. There’s also a complete React Overview and Walkthrough by Tania Rascia if you’re looking for a comprehensive guide to React in detail. 

Typescript adds a type system for type checking, refactoring, additional navigation features, and more. Once you start using Typescript with React you won’t go back. For a full overview of the benefits of using Typescript with React, check out this post about getting started with React and TypeScript.

Creating a React development environment

After reviewing React and Typescript, we need to set up the tools necessary to build a chat application in React.

Set up a Node.js development environment

If you’ve already installed Node.js, you can skip to the next section.

This tutorial requires you to install Node.js. To install Node.js you need to visit the Node.js downloads page and select the installer for your platform.

Next, we’ll check the installation. Open your terminal and run the following commands to ensure Node.js is installed:

node -v
npm -v

You should see a version number after you run these commands. 

Set up Git

You need Git to clone the React project from GitHub.

If you’ve already installed Git, you can skip to the next section.

Even if you have Git installed you may want to ensure that it’s the latest version. To do that follow the Getting Started - Installing Git guide.

Next, we’ll check the installation. Open your terminal and run the following command to ensure Git is installed:

git --version

You should see a version number after you run the command. 

Setting up a backend for chat app components 

To begin our tutorial on how to build a chat app using React, we’ll first, set up the backend we’ll use with our chat components. The end result will be two APIkeys that you’ll use in your React chat application. A PubNub account and API keys are free.To set up your backend:

  1. Sign up for a PubNub account.

  2. Go to your PubNub Dashboard.

  3. Click Create New App.

  4. Give your app a name, and select Chat App as the app type.

  5. Click Create.

  6. Click your new app to open its settings, then click its keyset.

  7. Enable the Channel Presence feature for your keyset.

  8. Enable the Storage and Playback feature for your keyset. 

  9. Enable the Stream Controller feature for your keyset.

  10. Save the changes.

  11. Copy the Publish and Subscribe keys for the next steps.

Building and running a React chat app

You’re ready to get started building with the Simple Chat Chat Kit repo.

First, open your terminal and clone the React project repo. This repository contains the open source PubNub powered chat components and chat UI that you’ll customize to build your chat application.

git clone https://github.com/PubNubDevelopers/chat-component-app-simple.git

Navigate into the repository. From here we’ll run and test the chat application.

cd chat-component-app-simple

Now you need to configure the components with the API keys you obtained from the PubNub dashboard. These keys allow you to send and receive messages on the PubNub network. You must also enable the Channel Presence feature, the Storage and Playback feature, and the Stream Controller feature for your API keys for the chat components to work properly with full functionality.

Open src/config/pubnub-keys.json. Replace YOUR_PUBLISH_KEY_HERE and YOUR_SUBSCRIBE_KEY_HERE with your keyset from your PubNub Dashboard.

Install the Node module dependencies used by the chat components. It’s normal for this process to take a few minutes to install the dependencies with npm.

npm install

Now you can run the project and try out each of the components. 

npm start

The React app should now open a html webpage at http://localhost:8080.

Try inputting text in the text input area and press enter. You should see your message appear in the message list area. If you open multiple tabs of the same application you should see more users appear in the active users list.

Chat component structure 

Let’s review the component structure now that you’ve tested the Simple Chat Kit. This chat application includes 3 components that are critical for most chat apps. Each of these components work together to provide a complete chat experience. These components allow you to build more functionality to customize them to your needs. 

  • Active Users: This component displays a list of the active users in the chat and a total count of users. This uses Presence to maintain the list of users in real-time. 

  • Message List: This component displays the messages received in the chat and the chat history when first loaded. This component displays the messages received from the PubNub subscription listener. 

  • Compose: This component provides an input area for sending new messages to the chat. When you press the enter key the contents of the text box are published to the PubNub network. Any users listening to the same channel and using the same API keys will receive the message.

React chat components explained

Now that you have tried out the chat UI and have an idea of how the React components function together to create a complete chat experience, let's break down how each of these components work and how you can modify them.

Component alignment

There are a few components that are components for aligning the chat components of the application. These components are:

ChatDemo:

This component outlines the structure for the majority of the application. If you navigate to the file src/components/ChatDemo/ChatDemo.tsx you’ll find the component structure. Notice how there are two main sides to this chat application: the active users component and the message list component.

    <ChatDemoWrapper>
      <ActiveUsersListPanelWrapper>
        <ActiveUsersListPanel />
      </ActiveUsersListPanelWrapper>
      <MessageListPanelWrapper>
        <MessageListPanel />
      </MessageListPanelWrapper>
    </ChatDemoWrapper>

ActiveUsersListPanel:

This component aligns the ActiveUsersList component.

MessageListPanel:

This component aligns the MessageList component and the compose text area. If you open the file src/components/MessageListPanel/MessageListPanel.tsx you’ll find the component structure. In this case the message list area is positioned above the compose text area.

      <MessageList />
      <ComposeMessageBoxWrapper>
        <UserImgWrapper src={state.selfAvatar} />
        <TextInputWrapper>
          <SendMessageField />
        </TextInputWrapper>
      </ComposeMessageBoxWrapper>

This panel also has an area to display the current user avatar. This avatar url is generated in src/AppStateContext.tsx when the initial state is defined from the name created by the generatedName() function. Consider this a placeholder for where you could provide a way for users to set custom avatars or use a popular avatar service like Gravatar.

 selfAvatar: "https://ui-avatars.com/api/?name="+generatedName+"?size=100&rounded=true&uppercase=true&bold=true&background=edab63&color=FFF", //The img for the avatar graphic file
  selfName: generatedName, // Set the display name.

Compose Component

The compose component can be found in the src/components/ComposeMessageBox directory. If you open the file ComposeMessageBox.tsx you’ll see that the component returns a text area. 

<SendMessageField></SendMessageField>

There’s a function for handling key press events that dispatches the SEND_MESSAGE function declared in the src/AppStateContext.tsx file. 

const handleKeyDown = (event) => {
    if (event.key === 'Enter') {
      dispatch({
        type: 'SEND_MESSAGE',
        payload: textAreaEl.current.value,
      })
      textAreaEl.current.value = ''
    }
  }

The SEND_MESSAGE function publishes the contents of the input field to the current channel defined in state. This is publishing a chat message. In this app there’s no changing channels. You’re only sending and receiving messages to the ‘global’ channel. The sender’s name, defined from the name created by the generatedName() function, is also appended to the published message.

 // Publishes a message to the chat channel.
    case "SEND_MESSAGE": {

      state.pubnub.publish({
        channel: state.channel,
        message: {
          "message": DOMPurify.sanitize(action.payload as string) as string,
          "senderName": state.selfName as string,
        },
      });

      return { ...state }
    }

Message List Component

The message list component can be found in the src/components/MessageList directory. If you open the file MessageList.tsx you’ll see that the component returns a list of messages from the messages in the state.messages array and scrolls to the bottom.

export const MessageList: React.SFC<MessageListProps> = (props: MessageListProps) => {
  const { state } = useAppState();
  const [stopOnScroll, setStopOnScroll] = useState(false);
  const messagesEndRef = useRef<null | HTMLDivElement>(null)
  const scrollToBottom = () => {
   messagesEndRef?.current?.scrollIntoView({ block: "end", inline: "nearest", behavior: "smooth" });
    }

  useScrollPosition(({ prevPos, currPos }) => {
     const isShow = currPos.y > prevPos.y
    if (isShow !== stopOnScroll) setStopOnScroll(isShow)
  }, [])

  useEffect(scrollToBottom, [state.messages])

  const Messages = Array.from(state.messages).map((onemessage: Array<any>, i: number) => {
    return (
      <React.Fragment key={i}>
        <Message message={onemessage} />
        <div ref={messagesEndRef} />
      </React.Fragment>
    );
  });

  return <MessageListWrapper>{Messages}</MessageListWrapper>
}

The Message subcomponent can be found in the src/components/Message directory. If you open the file Message.tsx you’ll see that messages are conditionally formatted depending on if you sent or received the message. Each message is formatted with an avatar (generated from the message sender name), the sender name, and the actual message. 

if (props.message.senderName == state.selfName) { // Display messages from yourself on the right.
        const userAvatar = "https://ui-avatars.com/api/?name="+props.message.senderName+"?size=100&rounded=true&uppercase=true&bold=true&background=edab63&color=FFF"
        return (
            <MessageWrapper key={props.message.internalKey}>
                <RightWrapper>
                    <SenderNameWrapperSelf>{props.message.senderName}</SenderNameWrapperSelf>
                    <AvatarWrapperSelf src={userAvatar} />
                </RightWrapper>
                <MessageMessageWrapperSelf><MessageMessageInnerWrapperSelf>{props.message.message}</MessageMessageInnerWrapperSelf></MessageMessageWrapperSelf>
            </MessageWrapper>
        )
    } else {
        const userAvatar = "https://ui-avatars.com/api/?name="+props.message.senderName+"?size=100&rounded=true&uppercase=true&bold=true&background=5EB977&color=FFF"
        return (
            <MessageWrapper key={props.message.internalKey}>
                <AvatarWrapper src={userAvatar} />
                <SenderNameWrapper>{props.message.senderName}</SenderNameWrapper>
                <MessageMessageWrapper><MessageMessageInnerWrapper>{props.message.message}</MessageMessageInnerWrapper></MessageMessageWrapper>
            </MessageWrapper>
        )
    }

The state.messages array is managed by the src/AppStateContext.tsx file. At the start of the application the history is requested from PubNub for the channel defined in state. In this app there’s no changing channels. You’re only sending and receiving messages to the ‘global’ channel and so we only need to get the history for the ‘global’ channel. Consider the following code:

 //Get the history on the default channel.
        state.pubnub.history(
            {
                channel: state.channel,
                count: state.historyMax // Limit of 100 messages.
            },
            (status, response) => { 
              if (response.messages.length > 0) {
                var historyMessages: Array<string> = [];
                for (var i = 0; i <= response.messages.length; i++) {
                  if (typeof response.messages[i] !== "undefined") {
                    response.messages[i].entry.message = DOMPurify.sanitize(response.messages[i].entry.message as string) as string;
                    historyMessages.push(response.messages[i].entry as string);
                  }
                }
                dispatch({
                  type: "ADD_HISTORY",
                  payload: historyMessages
                });
              }
            }
        );

The ADD_HISTORY function prepends the array returned from the history call to the message list array. The array is prepended because it’s possible for messages to be received before the history is able to return. The message list array size is also checked and trimmed to a manageable amount of messages. 

//ADD_HISTORY prepends an array of messages to our internal MessageList buffer.
    case "ADD_HISTORY": {

       const historyMerged: AppState = {
        ...state,
        messages: [
          ...action.payload as Array<string>,
          ...state.messages as Array<string>
        ]
      };

      //If the message list is over our cap we discard the oldest messages in the list.
      if (state.messages.length > state.maxMessagesInList) {
        state.messages.slice(state.messages.length-state.maxMessagesInList, state.messages.length);
      }

      return historyMerged;
    }

 At the start of the application the subscription for messages begins for the current channel defined in state. In this app there’s no changing channels. You’re only sending and receiving messages to the ‘global’ channel and so we only need to subscribe to the ‘global’ channel.

// Subscribe on the default channel.
      state.pubnub.subscribe(
        {
          channels: [state.channel], //Only one channel.
          withPresence: state.presence, 
        }
      );

Newly received messages from the subscription are received by a PubNub listener callback.

state.pubnub.addListener({
        message: (messageEvent) => {
          messageEvent.message.message = DOMPurify.sanitize(messageEvent.message.message as string) as string;
          dispatch({
            type: "ADD_MESSAGE",
            payload: messageEvent.message
          });
        }
});

Messages are added to the message list array using the ADD_MESSAGE function. The ADD_MESSAGE function adds the new message to the end of the array and discards old messages. 

//ADD_MESSAGE adds an incoming message to our internal MessageList buffer.
    case "ADD_MESSAGE": {
      //If the message list is over our cap we discard the oldest message in the list.
      if (state.messages.length > state.maxMessagesInList ){
        state.messages.shift();
      }

      const addMessage: AppState = {
        ...state,
        messages: [
          ...state.messages as Array<string>,
          {
            ...action.payload as string
          }
        ]
      };

      return addMessage;
    }

Presence Component

The active users component can be found in the src/components/ActiveUsersList directory. If you open the file ActiveUsersList.tsx you’ll see that the component returns a list of active users from the uuids in the state.activeUsers array and shows a count of the current occupancy in the header. The UUID for a user within the PubNub network is set to the same as the generated name in state for each user. This way you don’t need any additional information to visually display who a user is (no API lookup necessary).

const ActiveUsers = Array.from(state.activeUsers).map((activeUserName: string, i: number) => {
    return (
      <React.Fragment key={i}>
        <ActiveUser activeUser={activeUserName} />
        <div ref={activeUserEndRef} />
      </React.Fragment>
    );
  });

  return <><ActiveUsersHeader>Active 
Users<ActiveUsersOccupancy>{state.presenceOccupancy}</ActiveUsersOccupancy></ActiveUsersHeader><ActiveUsersListWrapper>{ActiveUsers}</ActiveUsersListWrapper></>

The ActiveUser sub component can be found in the src/components/ActiveUser directory. If you open the file ActiveUser.tsx you’ll see that user names are conditionally appended with “(You)” depending on if the user being listed is you. Each user name is displayed with an avatar generated from the UUID / user name.

let ActiveAvatarURL = "https://ui-avatars.com/api/?name="+props.activeUser+"?size=100&rounded=true&uppercase=true&bold=true&background=5EB977&color=FFF";
    let ActiveUserDisplay = props.activeUser; // Append " (You)" to the name displayed in the list to help a user identify themself while demoing.
    if (ActiveUserDisplay == state.selfName) {
        ActiveUserDisplay = ActiveUserDisplay + " (You)";
        ActiveAvatarURL = "https://ui-avatars.com/api/?name="+props.activeUser+"?size=100&rounded=true&uppercase=true&bold=true&background=edab63&color=FFF";
    }
    
    return (
        <ActiveUserWrapper>
            <ActiveUserAvatarWrapper src={ActiveAvatarURL} />
            <ActiveUserNameWrapper>{ActiveUserDisplay}</ActiveUserNameWrapper>
        </ActiveUserWrapper>
    )

The state.activeUsers array is managed by the src/AppStateContext.tsx file. At the start of the application a hereNow call is made to PubNub for the current channel occupants. We only need to get the occupants for the ‘global’ channel.

state.pubnub.hereNow(
          {
              channels: [state.channel],
              includeUUIDs: true // In this demo we're using the uuid as the user's name. You could also use presence state to provide a username and more. In this app all we need is the UUID of online users.
          },
          (status, response) => {
            if (response.channels[state.channel].occupancy > 0) {
              var newActiveUsers = state.activeUsers;
              for (var i = 0; i < response.channels[state.channel].occupancy; i++) {
                if (!state.activeUsers.includes(response.channels[state.channel].occupants[i].uuid)) {
                  newActiveUsers.push(response.channels[state.channel].occupants[i].uuid); 
                }
              }
              newActiveUsers.push(state.selfName); 
              newActiveUsers.sort(); // This prevents a users name from moving in the list.
              dispatch({
                type: "REFRESH_ACTIVEUSERS",
                payload: newActiveUsers
              });
              dispatch({
                type: "UPDATE_OCCUPANCY",
                payload: newActiveUsers.length
              });
            }
          }
        );

The “REFRESH_ACTIVEUSERS” function updates the state.activeUsers array with the list of users by UUID. The “UPDATE_OCCUPANCY” function updates the state.presenceOccupancy count. 

    //REFRESH_ACTIVEUSERS replaces array of users in our internal activeUsers buffer.
    case "REFRESH_ACTIVEUSERS": {
       const activeUsersList: AppState = {
        ...state,
        activeUsers: [
          ...action.payload as Array<string>
        ]
      };
      return activeUsersList;
    }
    //UPDATE_OCCUPANCY updates the current count of users
    case "UPDATE_OCCUPANCY": {

      const occupantsUpdate: AppState = {
        ...state,
        presenceOccupancy: action.payload as string
      };

      return occupantsUpdate;
    }

The active users list is updated as users join and leave the chat by a PubNub listener callback (the same one used for new messages). When a user joins the chat, the user’s uuid (user name) is appended to the existing activeUser array and refreshed along with the occupancy count. When a user leaves the chat, the user’s uuid (user name) is removed from the activeUser array and refreshed along with the occupancy count. 

presence: function(p) {
          if (p.action == "join") {
            if ((!state.activeUsers.includes(p.uuid)) ) { // Only add users if they are missing from the list.
              newActiveUsers.push(p.uuid); 
              newActiveUsers.sort();
              dispatch({
                type: "REFRESH_ACTIVEUSERS",
                payload: newActiveUsers
              });
              dispatch({
                type: "UPDATE_OCCUPANCY",
                payload: newActiveUsers.length
              });
              // Add to current count 
            }
          }
          if ((p.action == "timeout") || (p.action == "leave")) {
            var index = newActiveUsers.indexOf(p.uuid)
            if (index !== -1) {
              newActiveUsers.splice(index, 1);
              dispatch({
                type: "REFRESH_ACTIVEUSERS",
                payload: newActiveUsers
              });
              dispatch({
                type: "UPDATE_OCCUPANCY",
                payload: newActiveUsers.length
              });
            }
          }
        }

What other features can I add to my chat app?

uc_in-app-chat ui kit.png

Now that you have a complete open source chat web application, you can expand on it with more messaging features and customize the CSS. There’s a ton of room for customization, so consider the components of the React app discussed in this article as a template. You can expand further and use PubNub’s Chat UI to build functionality into your application like:

Find the PubNub SDK that’s right for your application and then check out these great chat resources to start building:

Want to get started building live chat or messaging into your app today? Get in touch with our sales team to quickly get your in-app chat up and running.

More from PubNub

How to Create a Dating App: 7 Steps to Fit Any Design
Insights6 minMar 15, 2023

How to Create a Dating App: 7 Steps to Fit Any Design

There are common underlying technologies for a dating app, and in this post, we’ll talk about the major technologies and designs...

Michael Carroll

Michael Carroll

How to Create a Real-time Public Transportation Schedule App
Build6 minMar 14, 2023

How to Create a Real-time Public Transportation Schedule App

How to use geohashing, JavaScript, Google Maps API, and BART API to build a real-time public transit schedule app.

Michael Carroll

Michael Carroll

How to Create Real-Time Vehicle Location Tracking App
Build2 minMar 9, 2023

How to Create Real-Time Vehicle Location Tracking App

How to track and stream real-time vehicle location on a live-updating map using EON, JavaScript, and the Mapbox API.

Michael Carroll

Michael Carroll

Talk to an expert