A Simple Chat App with React, Node and WebSocket (using hooks)
During the coronavirus lockdown, I suppose like a great many other people, we have been participating in a number of online activities with friends and family. During one of these we used an app to play a quiz where the quiz master released the questions and the participants selected the answer (multiple-choice) within a prescribed time limit. It seemed to me that this whole process must be using web sockets of which I knew very little. As this was something that I've always meant to look into but never had the need to, and with not much to do these days I decide to do some research into how this all worked.
I came across this tutorial on the web which seemed to be about the right sort of level for me. However there was a big issue with it, at least as far as I'm concerned, in that it was written with class-based components and over the last year or so I've tried to employ the new React way of hooks and function based components.
So ... as I was typing it in I converted the sample code to function-based components as I went along. What I came up with was the following:
For ChatInput.js:
import React, {useState} from 'react'
const ChatInput = ({ onSubmitMessage }) => {
const [message, setMessage] = useState('')
return(
<div>
<input
type="text"
placeholder="Enter message ..."
value={message}
onChange={e => setMessage(e.target.value)}
/>
<input
type="submit"
value="Send"
onClick={() => {
onSubmitMessage(message)
setMessage('')
}}
/>
</div>
)
}
export default ChatInput
For ChatMessage.js:
import React from 'react'
const ChatMessage = ({ name, message }) => (
<p>
<strong>{name}</strong> <em>{message}</em>
</p>
)
export default ChatMessage
And for Chat.js:
import React, {useState, useEffect} from 'react'
import ChatInput from './ChatInput'
import ChatMessage from './ChatMessage'
const URL = 'ws://localhost:3030'
const Chat = () => {
const [ name, setName ] = useState('Bob')
const [ messages, setMessages ] = useState([])
const [ ws, setWS ] = useState(new WebSocket(URL))
const submitMessage = messageString => {
const message = { name: name, message: messageString }
ws.send(JSON.stringify(message))
setMessages([message, ...messages])
}
useEffect(() => {
ws.onopen = () => {
console.log('connected')
}
ws.onmessage = evt => {
const message = JSON.parse(evt.data)
setMessages([message, ...messages])
}
ws.onclose = () => {
console.log('disconnected')
setWS(new WebSocket(URL))
}
}, [ws])
return (
<div>
<label htmlFor="name">
Name:
<input
type="text"
id="name"
placeholder="Enter your name ...."
value={name}
onChange={e => setName(e.target.value)}
/>
</label>
<ChatInput
onSubmitMessage={messageString => submitMessage(messageString)}
/>
{messages.map((mess, index) => (
<ChatMessage
key={index}
message={mess.message}
name={mess.name}
/>
))}
</div>
)
}
export default Chat
The server code server.js remained the same.
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 3030 })
wss.on('connection', (ws) => {
ws.on('message', (data) => {
wss.clients.forEach((client) => {
if ((client !== ws) && (client.readyState === WebSocket.OPEN)) {
client.send(data)
}
})
})
})
If you open up the page up in two separate windows and type into one of them the message should also get displayed in the other:

When two browser windows were side by side then the window that was sending messages worked correctly but the other window was only showing the latest message i.e. the message list was not being updated. To work correctly I had to add the variable messages to the dependency list for the useEffect hook. This updates the value of messages in the event handler for the websocket whenever it's value has changed so that the next time the socket receives a new message from the other window then this message is added to all the previous lists.
The useEffect hook now reads like this:
useEffect(() => {
ws.onopen = () => {
console.log('connected')
}
ws.onmessage = evt => {
const message = JSON.parse(evt.data)
setMessages([message, ...messages])
}
ws.onclose = () => {
console.log('disconnected')
setWS(new WebSocket(URL))
}
}, [messages, ws])
As a result we see the chat window behaviour is now correct:
