Building a Chat Application with React and Websockets

banner

In this post, we will learn how to create a full stack chat application using React, Node.js and the Websocket protocol.

By the end of this post, we will end up with an app that looks like this:

app screenshot

We will learn how to build:

  • The server, which will be a basic Node.js application
  • The client application, which we will build using React.js
  • Websockets - the protocol used to exchange live messages between the client and the server

Getting started - setting up our project

Let's get started by setting up the project folder. You will need to install Node.js, after which you can install the create-react-app command line tools:

npm i -g create-react-app

Once that's done, create you new project folder with the command:

create-react-app react-chat-example

This will create a new folder called react-chat-example. This folder will contain some files initially, which you can ignore for now.

All relative file paths from this point forward refer to the files within the project folder.

Creating the server

In order to build a chat application, we need a way to relay the messages sent by one user, to all the other users logged into the channel.

The server acts as the message hub: accepting messages from the connected client applications, and sending them to all the other connected client applications:

the server sends messages to other connected users

Websocket connections

The majority of the websites you visit make HTTP API calls, which means the client sends a request to the server, and the server sends back a response.

http request model

The problem here, is that this communication can only be initiated by the client. This is a problem if the server ever wants to notify the client at any random time.

Websockets are the answer to this problem.

Unlike an HTTP call, a Websocket connection remains open as long as both the client and server choose not to close it. While the connection is open, messages can be exchanged both ways:

websocket connection model

Let's create a server to accept incoming Websocket connections, using the express and express-ws library. First, install the library in your project folder:

npm i express express-ws

Next, create a new folder server, and create a file index.js within it.

// server/index.js

// import the express and express-ws libraries
const express = require('express')
const expressWs = require('express-ws')

// create a new express application
const app = express()
// decorate the app instance with express-ws to have it
// implement websockets
expressWs(app)

// Create a new set to hold each clients socket connection
const connections = new Set()

// We define a handler that will be called everytime a new
// Websocket connection is made
const wsHandler = (ws) => {
  // Add the connection to our set
  connections.add(ws)

  // We define the handler to be called everytime this
  // connection receives a new message from the client
  ws.on('message', (message) => {
    // Once we receive a message, we send it to all clients
    // in the connection set
    connections.forEach((conn) => conn.send(message))
  })

  // Once the client disconnects, the `close` handler is called
  ws.on('close', () => {
    // The closed connection is removed from the set
    connections.delete(ws)
  })
}

// add our websocket handler to the '/chat' route
app.ws('/chat', wsHandler)

// host the static files in the build directory
// (we will be using this later)
app.use(express.static('build'))

// start the server, listening to port 8080
app.listen(8080)

Every time a new client connects, we store the connection in the connections set. Once a message is received from any one of the connections, it is sent to the rest of them.

architecture of the above code

In order to start the server, execute the command:

node server/index.js

Creating the client application

Now that our server is setup, we can build the client web application. The create-react-app command would have already created a scaffold for a React application, with the initial files being within the src directory.

You can use the command:

npm start

to start the application in development mode and view it on your browser.

Let's start by creating the Websocket client. We don't need any external library this time, since most modern browsers support the Websocket API

Create a new file src/websocket.js, where we will include the logic required to maintain and interact with the Websocket connection:

// src/websocket.js

// The server host is determined based on the mode
// If the app is running in development mode (using npm start)
// then we set the host to "localhost:8080"
// If the app is in production mode (using npm run build)
// then the host is the current browser host
const host = process.env.NODE_ENV === 'production' ? window.location.host : 'localhost:8080'

// We create an exported variable `send`, that we will assign
// later (just know that it is exported for now)
export let send
// The onMessageCallback is also assigned later, as we will soon see
let onMessageCallback

// This exported function is used to initialize the websocket connection
// to the server
export const startWebsocketConnection = () => {
  // A new Websocket connection is initialized with the server
  const ws = new window.WebSocket('ws://' + host + '/chat') || {}

  // If the connection is successfully opened, we log to the console
  ws.onopen = () => {
    console.log('opened ws connection')
  }

  // If the connection is closed, we log that as well, along with
  // the error code and reason for closure
  ws.onclose = (e) => {
    console.log('close ws connection: ', e.code, e.reason)
  }

  // This callback is called everytime a message is received.
  ws.onmessage = (e) => {
    // The onMessageCallback function is called with the message
    // data as the argument
    onMessageCallback && onMessageCallback(e.data)
  }

  // We assign the send method of the connection to the exported
  // send variable that we defined earlier
  send = ws.send.bind(ws)
}

// This function is called by our React application to register a callback
// that needs to be called everytime a new message is received
export const registerOnMessageCallback = (fn) => {
  // The callback function is supplied as an argument and assigned to the 
  // `onMessageCallback` variable we declared earlier
  onMessageCallback = fn
}

There are a lot of functions and handlers being passed around here, so let's recap what we just did.

We need the application to be able to send and receive messages from the server. The websocket.js file acts as the interface between the rest of our client application code, and the Websocket server.

Sending messages

The send function in the above code is assigned only after the Websocket connection is established, and exported to allow our application code to call it, and in turn send messages.

send message flow

Receiving messages

Now to receive messages, the Websocket package will need to call some function that resides in our application code (the opposite direction as sending a message).

receive message flow

The registerOnMessageCallback function allows our application code to set the function that the websocket package will then call every time a new message is received.

In this way, we have separated out the application code and the Websocket client interface.

Creating our React components

We want to build a basic chat window where a user can send messages and view the messages sent by others. We can visualize this as a bunch of React components as follows:

react component architecture

  • The App component is the main container of our application
  • The MessageWindow is the wrapper component that holds all the messages (which are displayed as Message components)
  • The TextBar component is where the user can input text and send their messages

Displaying messages

We'll create a new file MessageWindow.jsx to house the MessageWindow, that acts as a container for all our messages, and the Message component itself.

// src/MessageWindow.jsx

import React from 'react'
// You can view the CSS at: https://github.com/sohamkamani/react-chat-example/blob/master/src/MessageWindow.css
import './MessageWindow.css'

// The message component takes the message text and the username of the message
// sender. It also takes `self` - a boolean value depicting whether the message
// is sent by the current logged in user
const Message = ({ text, username, self }) => (
  <div className={'message' + (self ? ' message-self' : '')}>
    <div className='message-username'>{username}</div>
    <div className='message-text'>{text}</div>
  </div>
)

// The message window contains all the messages
export default class MessageWindow extends React.Component {
  constructor (props) {
    super(props)
    // The `messageWindow` ref is used for autoscrolling the window
    this.messageWindow = React.createRef()
  }
  componentDidUpdate () {
    // Everytime the component updates (when a new message is sent) we
    // change the `scrollTop` attribute to autoscroll the message window
    // to the bottom
    const messageWindow = this.messageWindow.current
    messageWindow.scrollTop = messageWindow.scrollHeight - messageWindow.clientHeight
  }
  render () {
    const { messages = [], username } = this.props
    // The message window is a container for the list of messages, which
    // as mapped to `Message` components
    return (
      <div className='message-window' ref={this.messageWindow}>
        {messages.map((msg, i) => {
          return <Message key={i} text={msg.text} username={msg.username} self={username === msg.username} />
        })}
      </div>
    )
  }
}

The CSS for the above components can be viewed in the source code repository

Accepting Text Input

The text bar under the message window will be used to accept text input and send the message to the server if a "send" button is clicked, or if the enter key is pressed.

We will make the TextBar component in a new file TextBar.jsx:

import React, { Component } from 'react'
// The CSS can be viewed at https://github.com/sohamkamani/react-chat-example/blob/master/src/TextBar.css
import './TextBar.css'

export default class TextBar extends Component {
  constructor (props) {
    super(props)
    // Initialize a new ref to hold the input element
    this.input = React.createRef()
  }

  // The sendMessage method is called anytime the enter key is
  // pressed, or if the "Send" button is clicked
  sendMessage () {
    this.props.onSend && this.props.onSend(this.input.current.value)
    this.input.current.value = ''
  }

  // This method is assigned as the event listener to keypress events
  // if the key turns out to be the enter key, send the message by
  // calling the `sendMessage` method
  sendMessageIfEnter (e) {
    if (e.keyCode === 13) {
      this.sendMessage()
    }
  }
  render () {
    // Create functions by binding the methods to the instance
    const sendMessage = this.sendMessage.bind(this)
    const sendMessageIfEnter = this.sendMessageIfEnter.bind(this)

    // The textbar consists of the input form element, and the send button
    // The `sendMessageIfEnter` function is assigned as the event listener
    // to keydown events for the text input
    // The `sendMessage` function is assigned as the listener for the click
    // event on the Send button
    return (
      <div className='textbar'>
        <input className='textbar-input' type='text' ref={this.input} onKeyDown={sendMessageIfEnter} />
        <button className='textbar-send' onClick={sendMessage}>
          Send
        </button>
      </div>
    )
  }
}

The CSS for the TextBar component can be viewed in the source code repository

Creating the Application Component

Now that we have the text input and message display components in place, and the websocket adapter created, let's bind them together by creating our main application component.

We will update the existing App.jsx file:

import React from 'react'
// The CSS can be viewed at https://github.com/sohamkamani/react-chat-example/blob/master/src/App.css
import './App.css'

// We import all the components and functions that we defined previously
import MessageWindow from './MessageWindow'
import TextBar from './TextBar'
import { registerOnMessageCallback, send } from './websocket'

export class App extends React.Component {
  // The messages and username are used as the application state
  state = {
    messages: [],
    username: null
  }

  constructor (props) {
    super(props)
    // The onMessageReceived method is registered as the callback 
    // with the imported `registerOnMessageCallback`
    // Everytime a new message is received, `onMessageReceived` will
    // get called
    registerOnMessageCallback(this.onMessageReceived.bind(this))
  }

  onMessageReceived (msg) {
    // Once we receive a message, we will parse the message payload
    // Add it to our existing list of messages, and set the state
    // with the new list of messages
    msg = JSON.parse(msg)
    this.setState({
      messages: this.state.messages.concat(msg)
    })
  }

  // This is a helper method used to set the username
  setUserName (name) {
    this.setState({
      username: name
    })
  }

  // This method accepts the message text
  // It then constructs the message object, stringifies it
  // and sends the string to the server using the `send` function
  // imported from the websockets package
  sendMessage (text) {
    const message = {
      username: this.state.username,
      text: text
    }
    send(JSON.stringify(message))
  }

  render () {
    // Create functions by binding the methods to the instance
    const setUserName = this.setUserName.bind(this)
    const sendMessage = this.sendMessage.bind(this)

    // If the username isn't set yet, we display just the textbar
    // along with a hint to set your username. Once the text is entered
    // the `setUsername` method adds the username to the state
    if (this.state.username === null) {
      return (
        <div className='container'>
          <div className='container-title'>Enter username</div>
          <TextBar onSend={setUserName} />
        </div>
      )
    }

    // If the username is already set, we display the message window with
    // the text bar under it. The `messages` prop is set as the states current list of messages
    // the `username` prop is set as the states current username
    // The `onSend` prop of the TextBar is bound to the `sendMessage` method
    return (
      <div className='container'>
        <div className='container-title'>Messages</div>
        <MessageWindow messages={this.state.messages} username={this.state.username} />
        <TextBar onSend={sendMessage} />
      </div>
    )
  }
}

export default App

Running the Application

For development mode, your application should be working as expected. The server can be started with:

node server/index.js

and the react development server can be started with:

npm start

The application can be viewed at localhost:3000.

Creating a production build

To run the application in production mode (with minified and compressed assets, and a single server), first run the command:

npm run build

This will compile and build the client-side assets, and put them inside the build folder.

In our server, we added the line:

app.use(express.static('build'))

This directs the server to serve everything in the build directory as static assets. Now when you run the server, you can visit localhost:8080 to see your production grade react chat application.

You can find the complete source code for this project in the Github repository

Have any feedback? Let me know in the comments!


Liked this article? Share it on: