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:
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
If you just want to see the code for this project, you can view the Github repository
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 run the create-react-app command line tools:
npx 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:
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.
However, this type of 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:
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.
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.
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).
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:
- The
App
component is the main container of our application - The
MessageWindow
is the wrapper component that holds all the messages (which are displayed asMessage
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!