Socket.io sends back multiple responses only to the sender - javascript

so I wanted to dive into Socket.io and I am a complete beginner in it. I am a react developer and decided to test it with react, this is my client side:
import { io } from "socket.io-client";
function App() {
let [message, setMessage] = useState("");
let [messages, setMessages] = useState([]);
const socket = io("http://localhost:4000/", {
withCredentials: true,
cors: {
origin: "http://localhost:4000",
},
});
const sendMessage = (msg) => {
socket.emit("messageToServer", msg);
};
socket.on("messageToClient", (msg) => {
setMessages((prevMessage) => prevMessage.concat(msg));
});
return (
<div className="App">
{messages}
<input
id="input"
autoComplete="off"
onChange={(e) => setMessage(e.target.value)}
/>
<button onClick={() => sendMessage(message)}>Send</button>
</div>
);
}
and this is my server side(Node, Express):
const app = require("express")();
const http = require("http").Server(app);
const io = require("socket.io")(http, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"],
credentials: true,
},
});
io.on("connection", (socket) => {
let messages = [];
socket.on("messageToServer", (msg) => {
io.emit("messageToClient", msg);
console.log(msg);
});
});
http.listen(4000, () => {
console.log("listening on *:3000");
});
So basicly when I write something on the input and click the button, on the node server everything seems fine and the message logs only once, but when I open the tab that I send the message from and for example I typed 'First message' it will print something like: 'First messageFirst messageFirst messageFirst message' and so on...
But apparently, the duplication is happening only from the tab I am sending it from, because when I open a new one and test it, on the sender tab I get multiple like i said, but on the new one, I recieve it only one, just like it should be.
Thank you in advance!

The below snippet of code will keep App component being rendered multiple times, thus the same event handler for messageToClient will be registered multiple times and cause unexpected behavior in React.
socket.on("messageToClient", (msg) => {
setMessages((prevMessage) => prevMessage.concat(msg));
});
You should wrap socket on event with useEffect, so that we can make sure nothing is duplicating.
useEffect(()=>{
socket.on("messageToClient", (msg) => {
setMessages((prevMessage) => prevMessage.concat(msg));
});
// unbind the event handler when the component gets unmounted
return () => {
socket.off('messageToClient')
}
},[])

Related

Nodejs global variables change unpredictably in response to the requests

I am working on an app that needs to count the number of users that are viewing a webpage at the same time. To do that, I created a React component that is responsible for the page display, and every time when it loads it sends a request to the Node.JS backend whose sole purpose is to keep track of the amount of users. Every time the page loads, it sends the get request to Node to get the amount of users and displays it as a state, and every time the user leaves the page the event listener must send a post request to decrement the value of variable by one. This is what I have so far:
server.js (back-end)
//This is a small backend that keeps track of how many people are on each page.
const express = require('express')
const cors = require('cors')
const app = express()
const whiteList = ['http://localhost:3000', 'http://127.0.0.1:3000']
const corsOptions = {
origin: function (origin, callback) {
if (!origin || whiteList.indexOf(origin) !== -1)
callback(null, true)
else
callback(new Error("Not allowed by CORS"))
},
credentials: true,
}
app.use(cors(corsOptions))
let googleCounter = 0, discordCounter = 0, linuxCounter = 0
console.log(googleCounter, discordCounter, linuxCounter)
app.get("/google", (request, response) => {
console.log(googleCounter, discordCounter, linuxCounter)
console.log(`Google counter ${googleCounter} increased to ${googleCounter + 1}`)
googleCounter++
response.send(`Google Counter: ${googleCounter}`)
})
app.get("/discord", (request, response) => {
discordCounter++
response.send(discordCounter)
})
app.get("/linux", (request, response) => {
linuxCounter++
response.send(linuxCounter)
})
//These methods will fire when users leave the pages.
app.post("/google", () => {
console.log(`Google counter ${googleCounter} decreased to ${googleCounter - 1}`)
googleCounter--})
app.post("/discord", () => discordCounter--)
app.post("/linux", () => linuxCounter--)
app.listen(5000, () => console.log(`Server started on port 5000: ${googleCounter}, ${discordCounter}, ${linuxCounter}`))
Google.js (the front-end)
//This is a generic page to view. Source: https://uk.wikipedia.org/wiki/Google
import React, {useState, useEffect} from "react"
import Base from "./Base"
export default function Google(props) {
const url = "http://localhost:5000/google"
const [counter, setCounter] = useState(1)
//Increment user counter when the page is loaded:
useEffect(() => {
if (!props.user) return;
fetch(url, {method: "GET"})
.then(response => {return response.text()})
.then(response => console.log(response))
.catch(error => console.log(error))
//Decrement user counter when the user leaves the page:
window.addEventListener("unload", () => fetch(url, {method: "POST"})
.then(response => {return response.text()})
.then(response => console.log(response))
.then(() => console.log("User left the page")))
.catch(error => console.log(error))
}, [props.user])
if (!props.user) return (<div><Base/></div>)
return (<div>
<h1>Google</h1>
<h2>Currently active users: {counter}</h2>
</div>)
}
My issue is that the googleCounter variable behaves unpredictably: when I run the server, it immediately falls to -3 to -5, and every time I send the get request it increments the variable but also decrements. I suspect the unload event could run more often than when user leaves the page, useEffect could have run more often or the get request could overlap with the post request in some way. When I tried to log the post request from the client-side, I've got the event listener is undefined:
How can I make sure that the googleCounter variable only decreases when the user leaves the page?

bypass service worker for a specific request

I have an react app and I am using a service worker for offline purpose, I have a custom hook, this hook sends a request to check connection. If the request return a response of 200 it mean the user is online else offline. Currently I am getting a 200 ok from service worker, but the network is off. I would like to bypass the service worker and let the request run normally. The goal is to ensure the request does not return 200 while offline
here is my hook :
import { useEffect, useState } from 'react';
export const useOnline = () => {
const [online, setOnline] = useState(true);
const checkOnline = async () => {
try {
const response = await fetch('/hello.png');
setOnline(response.ok);
} catch {
setOnline(false);
}
};
useEffect(() => {
const change = () => {
setOnline(!online);
};
checkOnline();
window.addEventListener('online', () => change());
window.addEventListener('offline', () => change());
return () => {
window.removeEventListener('online', () => change());
window.removeEventListener('offline', () => change());
};
}, [online]);
return online;
};
I was thinking to add this in my service worker
self.addEventListener('fetch', function (event) {
if (event.request.url.match('^.*(/hello.png/).*$')) {
return false;
}
});
Not possible, because your hook will automaticly send 200 message.
It's a violation of service worker's goal, and your code will not work

React socket.io-client not re-rendering component after response from server to a particular room (Python backend)

I have a server backend written in Python with Flask-SocketIO. I'm utilizing it's room feature to make private conversations. Upon a join room event the server fires the following function to let the frontend know where to send messages to specific user:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient}, to=sid)
where sid is the private room created only for the user when connecting to a socket. Then I want to keep this information in React state in a map, like this:
function ChatWindow({ username, token }) {
const [responses, setResponses] = useState([]);
const [roomsMap, setRoomsMap] = useState(new Map());
const [currentRoom, setCurrentRoom] = useState("");
const [messageValue, setMessageValue] = useState("");
var socket = null;
useEffect(() => {
socket = socketIOClient(ENDPOINT);
});
useEffect(() => {
socket.on("global response", (data) => {
setResponses((responses) => [...responses, data]);
});
socket.on("room name response", (data) => {
console.log(`joined ${data.roomName} with ${data.recipient}`);
setCurrentRoom((currentRoom) => data.roomName);
setRoomsMap((roomsMap) => roomsMap.set(data.recipient, data.roomName));
});
return () => socket.close();
}, []);
const sendMessage = () => {
if (messageValue.length < 1) {
return;
}
socket.emit("global message", {
user_name: username,
message: messageValue,
timestamp: Date.now(),
});
setMessageValue("");
};
const joinRoom = (recipient) => {
socket.emit("join", {
token: token,
username: username,
recipient: recipient,
});
// setCurrentRoom(() => roomsMap.get(recipient));
};
const leaveRoom = (recipient) => {
socket.emit("leave", {
token: token,
username: username,
recipient: recipient,
});
const newRooms = roomsMap;
newRooms.delete(recipient);
console.log(`left room with ${recipient}`);
newRooms.forEach((val, key) => console.log(`${val}:${key}`));
setRoomsMap(newRooms);
};
const checkUser = (userToCheck) => {
if (userToCheck === username) {
return styles.chatFromUser;
} else {
return styles.chatToUser;
}
};
return (...);
}
export default ChatWindow;
Sadly, React doesnt react to the socket emitting message, even though it can be seen in network tab in developer tools. The global response works fine.
When I alter the backend function to:
socketio.emit('room name response', {'roomName': room_name, 'recipient': recipient})
React suddenly works as expected. I'm trying to understand why it happens, especially when the browser seems to see the incoming messages as stated above, so it's most likely my bad coding or some React/Javascript thing.
Thank You for any help in advance.
The problem was that socket sometimes was created multiple times, therefore, the socket that useEffect was currently listening wasn't necessarily the one in the room. So I made one, global socket to fix this and whole thing now works.

How to receive SocketIO events in React while using states

This is part of my code, what I want to do is this component at any time can receive a message on any of the conversations. Sending a message triggers a Socket event which triggers this code below, but I can't seem to get the "latest" conversations, as the useEffect only triggers when the component mounts (at that point my conversations array has zero length).
What I was thinking is that I should include "conversations" on the useEffect's dependency but that would create multiple websocket connection, one each time a Socket.io event is triggered because it does change the state. Is this the best solution? Thanks in advance!
const [conversations, setConversations] = useState<Array<Conversations>>([]);
useEffect(() => {
async function getConversations() {
try {
const { data } = await axios.get("/api/conversations/");
if (data.success) {
setConversations(data.details);
}
} catch (err) {}
}
getConversations();
socketInstance.on("connect", () => {
console.log("Connecting to Sockets...");
socketInstance.emit("authenticate", Cookies.get("token") || "");
});
socketInstance.on("ackAuth", ({ success }) => {
console.log(
success
? "Successfully connected to Sockets"
: "There has been an error connecting to Sockets"
);
});
socketInstance.on("newMessage", (data) => {
const modifiedConversation: Conversations = conversations.find(
(conv: Conversations) => {
return conv.conversationId === data.conversationId;
}
);
modifiedConversation.messages.push({
from: {
firstName: data.firstName,
lastName: data.lastName,
profilePhoto: data.profilePhoto,
userId: data.userId,
},
content: data.content,
timeStamp: data.timeStamp,
});
const updatedConversations = [
...conversations.filter(
(conv) => conv.conversationId !== data.conversationId
),
modifiedConversation,
];
setConversations(updatedConversations);
});
}, []);
While attaching and removing the socket listeners every time conversations changes is a possibility, a better option would be to use the callback form of the setters. The only time you reference the state, you proceed to update the state, luckily. You can change
socketInstance.on("newMessage", (data) => {
const modifiedConversation: Conversations = conversations.find(
// lots of code
setConversations(updatedConversations);
to
socketInstance.on("newMessage", (data) => {
setConversations(conversations => {
const modifiedConversation: Conversations = conversations.find(
// lots of code
setConversations(updatedConversations);
You should also not mutate the state, since this is React. Instead of
modifiedConversation.messages.push({
do
const modifiedConversationWithNewMessage = {
...modifiedConversation,
messages: [
...modifiedConversation.messages,
{
from: {
// rest of the object to add

Create chatrooms dynamically in socket.io

I'm developing a chat app and i have some trouble with socket.io.
Stuff works great and quick. But as soon as more people are logged in - everyone see's all the messages and rendering gets messy.
I'm sending a unique ID with each message, which i would love to create rooms with where people can chat with each other (1 on 1)
THis happens first:
const socketMessage = {
type: type,
body: body,
author: author,
date: Date.now(),
id: chatId
};
const io = require('socket.io-client');
const socket = io(process.env.REACT_APP_WS_URL);
socket.emit('message-sent', socketMessage);
THis goes to the server where this happens:
socket.on('message-sent', data => {
socket.broadcast.emit('new-chat-message', data);
});
WHich then goes into a custom hook and adds to the messages-array.
export default function useGetChatMessages(_id) {
const [chatMessages, setChatMessages] = React.useState([]);
React.useEffect(() => {
const io = require('socket.io-client');
const socket = io(process.env.REACT_APP_WS_URL);
socket.emit('send-chat-id', _id);
socket.on('chat-messages', data => {
setChatMessages(data.messages);
});
socket.on('new-chat-message', message => {
setChatMessages(messages => [...messages, message]);
notification.play();
});
}, [_id]);
return chatMessages;
}
I know the broadcast part is wrong on my server but i have tried lots of things:
Sending the chatId with the message and then do socket.join(chatId) at several stages.
Then socket.to(chatId).emit(...) and so on.
But i can not get it right.
Can someone help?
Thanks a lot in advance!! :-)
Actually, you could use the socket io feature called namespaces.
Look an pretty simple example:
In the client side you would do something like this:
const socket = io.connect(socket_url, { query: "chatRoomId=123&userId=983912" });
And in the server side something like this:
io.on('connection', (socket) => {
const chatRoomId = socket.handshake.query['chatRoomId'];
const userId = socket.handshake.query['userId'];
const user = find(userId) // Pretend :)
socket.join(chatRoomId);
socket.on('new_message', (id, msg) => {
io.to(chatRoomId).emit(`${user.name} says ${msg}`);
});
socket.on('disconnect', () => {
socket.leave(chatRoomId)
console.log('user disconnected');
});
});

Categories