So last time we talked about a simple timer with the use of setInterval. Great... so we have the stopwatch for the show. But how is that going to travel to our audience? The solution is sockets. My Express backend is integrating a socket server on the same port as the app so everything can co-exist happily. So basically everything is wrapped up nicely in emit events. We will go over the chat functionalities in this blog and how I created the timer scenario next time.
The project called for multiple chatrooms and private rooms (however these private rooms became multi-purpose as actors swapped in at different times from different computers). The basic set up in the backend is very reminiscent of React hooks. This gave me a warm fuzzy feeling if I can be completely honest, since this was a new technology for me. I will not go through in depth on how to make sure the server talking to the app in Node, this is more of a review of the interior for this use case. Feel free to reach out for other information
So the first thing you need to do is make sure there's a connection... makes sense... the last thing you need to do is make sure there's a disconnect... ooo basically useEffect with a return... and funny enough these will be mimicked like this in the front end.
So backend in it's simplest use.
module.exports = (function (app, port) {
const server = require('http').createServer(app);
const options = {
cors: {
origin: '*',
}
};
const io = require('socket.io')(server, options);
server.listen(port);
io.on("connection", (socket) => {
const { roomId, username } = socket.handshake.query;
socket.join(roomId);
socket.on("disconnect", () => {
socket.leave(roomId);
});
})
}
)
Some quick questions that may pop up... what is app, and port... Those will be passed in from your main app.js (I chose to keep sockets in a separate file thinking it may get a little messy), app is just your express call and the port will vary, I set mine to default at 5000 but in deployment on heroku it changes with the deployment. Cors for sanity sake is set to * so it can come wherever in your development environment in production you will set this to where you are being hosted.
Next questions, where the heck are roomId and username coming from? Well that's a more complicated story for the rest of the app that we won't dive too much into here. Username comes from their signup and travels through redux, and roomId is actually their location in our little play but you can repurpose those for right now. The handshake.query is the important part. The handshake is initiated on the React side and the query is where we are throwing our information.
For our frontend... I hilariously built my own hooks, but there are many standardized libraries out there use-socket and use-immer. But the begginning of our hook will look like this:
import { useEffect, useRef, useState} from "react";
import socketIOClient from "socket.io-client";
const SOCKET_SERVER_URL = `/`;
const useChat = (roomId, username) => {
const socketRef = useRef();
useEffect(() => {
socketRef.current = socketIOClient(SOCKET_SERVER_URL, {
query: { roomId, username }
});
return () => {
socketRef.current.disconnect();
};
}, [roomId]);
}
export {useChat}
Here we are using the React library of socketIOClient. It does much of the work for us.
The socket server url is set to "/" since it will be based off our server (works for localhost and deployed versions) but here is the handshake we were talking about. In that socketIOClient call, we are querying with our roomId and username, this happens on connection. Also you may be asking why socketRef is part of useRef(), well useRef() does not force a re-render, which could send us into a nasty little spiral if one of the socket calls emits something in particular. And mentioned earlier the unmount (really why don't they call it dismount?!?!) has the disconnect which closes us out just in case. You may ask why I have roomId in there as a dependency. The fact is we are having multiple rooms because there are several acts, so we don't want someone in Act 3 talking to Act 2 people, spoilers!
Due to this being a hook, this is called in a React component which is really what has my chat room set up. So that means we need to return some information for it to be rendered in it's host component. And we also need there to be some sort of functionality in the backend. To get the chat up and running we need a send and receive on both ends... and then the socket functions in react need to return that to the component.
So in the React side, lets get messages and add a send message function. The entire file would look something like this:
import { useEffect, useRef, useState } from "react";
import socketIOClient from "socket.io-client";
const NEW_CHAT_MESSAGE_EVENT = "newChatMessage";
const SOCKET_SERVER_URL = `/`;
const useChat = (roomId, username) => {
const [messages, setMessages] = useState([]);
const socketRef = useRef();
useEffect(() => {
socketRef.current = socketIOClient(SOCKET_SERVER_URL, {
query: { roomId, username }
});
socketRef.current.on(NEW_CHAT_MESSAGE_EVENT, (message) => {
const incomingMessage = {
...message,
ownedByCurrentUser: message.senderId === socketRef.current.id,
};
setMessages((messages) => [...messages, incomingMessage]);
});
return () => {
socketRef.current.disconnect();
};
}, [roomId]);
const sendMessage = (messageBody) => {
socketRef.current.emit(NEW_CHAT_MESSAGE_EVENT, {
body: messageBody,
senderId: socketRef.current.id,
});
return {
body: messageBody,
senderId: socketRef.current.id,
}
};
return { messages, sendMessage};
};
export {useChat};
We return all the messages as they get added and have the ability to send a message as the message body. The senderId is generated by sockets, which I had some issues with later on with reconnection and how I was rendering... so that's where the username came in but we will get to that another time.
So taking a look we are both on socketRef.current (this is the channel) but we have two functions following that, .on and .emit. ".on" means when the current used hears that socket call and .emit is sending it. Both of these are encapsulated in our room since we have joined it in our socket query.
Now let's look at how the backend is handling this.
const NEW_CHAT_MESSAGE_EVENT = "newChatMessage";
socket.on(NEW_CHAT_MESSAGE_EVENT, (data) => {
io.in(roomId).emit(NEW_CHAT_MESSAGE_EVENT, data);
});
Wait that's only one function... Exactly. We are not optimistically rendering the chat item and we are emitting in the channel. So every time the backend receives a message it sends it out. There are other socket functions that send to all but the sender but I opted against that since I was testing it on my home network and thought that would give too many false positives. So everytime this gets a message it sends it out to all sockets in that current room that we have joined. Since the room names are specific we can be sure they are going to the right places. This gets updated into the messages array which is returned by our custom hook, and since we had a useRef, it is keeping everything happy and no re-rendering.
To quickly gloss over the chat component, as long as you have a submit or click handler receiving the sendMessage function and give it the body of the message you are set, in mine I added the username to the beginning instead of the id which is basically a hash. Then rendered that information. Use cases may vary. Next time we will get into the silliness of sending time this way.
Top comments (0)