Why is socket.io not emitting to everyone within a room? - node.js

We are creating a multiplayer quiz app that uses socket.io and React. We have got a create room and join room working, but when the host in the lobby starts the game only they progress to the question page, and no one else in the lobby goes anywhere.
This is our code in app.js
import io from "socket.io-client";
const URL = "http://localhost:3001"
const {
difficulty,
category,
socket,
setSocket,
setAllPlayers,
setUserData
} = useContext(QuizContext)
useEffect(() => {
const newSocket = io(URL)
newSocket.on("update_room", (users) => {
setAllPlayers(users);
setUserData(prev => {
return {...prev, room: users[0]?.room}
});
});
}, [])
useEffect(() => {
if(socket) {
socket.on("game_started", () => {
console.log(2);
navigate("/quiz")
});
}
}, [socket]);
This is the code in WaitingRoom.js
<button onClick={startQuiz}>Start</button
const startQuiz = () => {
socket.emit("start_quiz", { room: userData.room, data: userData.data });
};
This is the code in our backend on socketEvents/index.js
socket.on("start_quiz", ({ room, data }) => {
users = users.map((user) => {
return { ...user, data };
});
io.to(room).emit("game_started", users);
});
We've looked at all the socket.io documentation and can't figure out why it's not working.

Related

State is null when trying to access it in the parent component, but able to pass it down to child component

EDIT: Simplified the code a bit by using two separate states. Adding the channel state to the dependency array of useEffect() does not remove the error but does make the application run in a strange manner: If I try to send messages, I see multiple messages.
EDIT 2: If I use the useRef() hook to store the state, my application works albeit in a strange manner again: the state is one step slower than the actual user input.
I'm trying to build a simple chat application using React.js and Socket.IO where a single user can communicate with multiple users. I have an endpoint from where I fetch all my channel data and store it in a state but I'm not able to access the state (returns null) even if the state is being passed down to a different component as props and that is being rendered correctly the first time. When I click on a channel to select it, the channel list is again reset to null and I get this error:
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'forEach')
I'm trying to implement this by following the tutorial from here. It's outdated, so I'm implementing a newer version of it. But even then, if I use the component class defined in the author's github, it seems to work just fine. I have not worked with component classes before, so I am not sure where I am going wrong.
Main component: Chat.js
import React, { useEffect, useState } from 'react';
import ChannelList from './ChannelList';
import './chat.css';
import MessagePanel from './MessagePanel';
import { io } from "socket.io-client";
const SERVER = "http://localhost:8080";
let socket;
function Chat() {
const [channel, setChannel] = useState(null)
const [channels, setChannels] = useState(null) //the state where the channels are loaded
const channelsRef = useRef() //edit 2
useEffect(() => {
loadChannels() //the function which loads the channels into the state
configureSocket()
}, []) //adding channel to the dependency array results in strange behavior.
const configureSocket = () => {
socket = io(SERVER)
socket.on('connection', () => {
if(channel) {
handleChannelSelect(channel.id)
}
});
socket.on('channel', channel => {
console.log('channel details', channel); //returns the correct channel clicked on
//let temp_channels = channels; //edit 2
let temp_channels = channelsRef.current; //edit 2
console.log('channels inside onchannel', temp_channels); //returns null, does not return null if useRef() is used
temp_channels.forEach(c => { //I get the above mentioned error at this line, after edit 2 I don't get any error
if(c.id === channel.id) {
c.participants = channel.participants
}
})
//setChannels(channels); edit 2
channelsRef.current = temp_channels // edit 2
})
socket.on('message', message => {
// let temp_channels = channels
let temp_channels = channelsRef.current //edit 2
temp_channels.forEach(c => {
if (c.id === message.channel_id) {
if (!c.messages) {
c.messages = [message];
} else {
c.messages.push(message);
}
}
})
// setChannels(channels) edit 2
channelsRef.current = temp_channels // edit 2
})
}
const handleSendMessage = (channel_id, text) => {
socket.emit('send-message', { channel_id, text, senderName: socket.id, id: Date.now()})
}
const handleChannelSelect = (id) => {
const channel = channels.find(c => {
return c.id === id;
});
setChannel(channel);
socket.emit('channel-join', id, ack => {});
}
const loadChannels = async () => {
await fetch('http://localhost:8080/getChannels')
.then(async response => {
const data = await response.json()
console.log('data inside loadchannels', data.channels);
setChannels(data.channels); //this sets the channels and passes it down to the child component "ChannelList"
channelsRef.current = data.channels //if setChannels is not used before this, it doesn't work
})
console.log('after fetching channels inside loadchannels', channel, channels); //both channel and channels state are null
}
return (
<div className="chat-app">
<ChannelList channels={channels} onSelectChannel={handleChannelSelect}></ChannelList>
<MessagePanel onSendMessage={handleSendMessage} channel={channel}/>
</div>
);
}
export default Chat;
ChannelList.js
import React from 'react';
import Channel from './Channel';
function ChannelList(props) {
const handleClick = (id) => {
props.onSelectChannel(id);
}
console.log('inside channellist component', props.channels); //returns the set of channels received from props
let list = <div className="no-content-message">There are no channels to show</div>;
if (props.channels && props.channels.map) {
list = props.channels.map(c =>
<Channel
key={c.id}
id={c.id}
name={c.name}
participants={c.participants}
onClick={handleClick}
/>
);
}
return (
<div className='channel-list'>
{list}
</div>
);
}
export default ChannelList;
Backend index.js:
const express = require('express');
const app = express();
http = require('http');
const cors = require('cors');
const { Server } = require('socket.io');
app.use(cors());
const server = http.createServer(app);
const STATIC_CHANNELS = [
{
name: 'Global chat',
participants: 0,
id: 1,
sockets: []
},
{
name: 'Funny',
participants: 0,
id: 2,
sockets: []
},
{
name: 'Test',
participants: 0,
id: 3,
sockets: []
},
];
const io = new Server(server, {
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST'],
},
});
server.listen(8080, () =>
console.log('Server is running on port 8080')
);
io.on('connection', (socket) => {
console.log(`User connected ${socket.id}`);
socket.emit('connected', socket.id);
socket.on('channel-join', id => {
console.log('channel join', id);
STATIC_CHANNELS.forEach(c => {
if (c.id === id) {
if (c.sockets.indexOf(socket.id) == (-1)) {
c.sockets.push(socket.id);
c.participants++;
io.emit('channel', c);
}
} else {
let index = c.sockets.indexOf(socket.id);
if (index != (-1)) {
c.sockets.splice(index, 1);
c.participants--;
io.emit('channel', c);
}
}
});
return id;
})
socket.on('send-message', message => {
io.emit('message', message);
})
socket.on('disconnect', () => {
STATIC_CHANNELS.forEach(c => {
let index = c.sockets.indexOf(socket.id);
if(index != (-1)) {
c.sockets.splice(index, 1);
c.participants--;
io.emit('channel', c);
}
})
})
});
app.get('/getChannels', (req, res) => {
res.json({
channels: STATIC_CHANNELS
})
})

Socket.io with express, specific room

I want to send event to only an specific client when an express rute is triggered.
I keep online users in io.onlineUsers:
socket.on("log-in", (userWithRoles) => {
const userSocket = {
id: userWithRoles.id,
roles: userWithRoles.roles,
socketId: socket.id,
};
if (io.onlineUsers === undefined) {
io.onlineUsers = [
{ ...userSocket, socketIds: [userSocket.socketId], room: socket.id },
];
} else {
if (
io.onlineUsers.map((uSocket) => uSocket.id).includes(userSocket.id)
) {
const existingUserSocket = io.onlineUsers.find(
(us) => us.id === userSocket.id
);
existingUserSocket.socketIds.push(userSocket.socketId);
socket.join(existingUserSocket.room);
return;
}
io.onlineUsers.push({
...userSocket,
socketIds: [userSocket.socketId],
room: socket.id,
});
}
});
I attach io via:
httpsServer.listen(process.env.PORT || 5000, () => {
const io = createIo(httpsServer);
app.io = io;
console.log(
`Server running on port ${
process.env.PORT || 5000
}. Go to https://localhost:5000/.`
);
});
I am looking for room/socketId in a router via:
const getAllCookRoles = async (req, res) => {
const cookRoles = await CookRoleUser.find()
.populate({ path: "user", select: "username name" })
.populate({ path: "cooks" });
console.log(req.app.io.onlineUsers);
const cookRoom = req.app.io.onlineUsers?.find((ou) =>
ou.roles.map((ouRole) => ouRole.name).includes(COOK)
)?.room;
console.log(cookRoom);
if (cookRoom) {
console.log(cookRoom);
req.app.io.sockets.to(cookRoom).emit("page-visit");
}
res.json(cookRoles);
};
And the client listener is:
useEffect(() => {
const socket = io("https://localhost:5000");
// receive a message from the server
socket.on("new-order", (socketData) => {
console.log("neworder");
console.log(socketData);
});
socket.on("page-visit", () => {
console.log("page-visit");
})
}, []);
When i do req.io.emit("page-visit") everyithing works like a charm.
Contrarly, besides fiding and logging the cook room, when i do req.app.io.sockets.to(cookRoom).emit("page-visit"); nothing happens. With .sockets part and w/out.
How could I send event to specific room, not everyone, from express route?

How to use socketio inside controllers?

I have a application created with Reactjs,Redux,Nodejs,MongoDB. I have created socketio in backend side.
server.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const app = express();
const routes = require('./routes/api/routes');
var server = require('http').Server(app);
const io = require('./socket').init(server);
app.use(express.json());
require('dotenv').config();
mongoose.connect(process.env.MONGO_DB).then(console.log('connected'));
const corsOptions = {
credentials: true, //access-control-allow-credentials:true
optionSuccessStatus: 200,
};
app.use(cors(corsOptions));
app.use(routes);
if (
process.env.NODE_ENV === 'production' ||
process.env.NODE_ENV === 'staging'
) {
// Set static folder
app.use(express.static('client/build'));
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
});
}
io.on('connection', (socket) => {
console.log('New client connected');
socket.on('disconnect', () => {
console.log('Client disconnected');
});
});
server.listen(process.env.PORT || 5000, () => {
console.log('socket.io server started on port 5000');
});
socket.js
let io;
module.exports = {
init: (httpServer) => {
return (io = require('socket.io')(httpServer, {
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST'],
},
}));
},
getIO: () => {
if (!io) {
throw new Error('Socket.io is not initialized');
}
return io;
},
};
In controller.js I am creating new item to inside mongodb and also using socketio. I am emitting new Item that I created. It looks like that
controller.js
const createTeam = async (req, res) => {
const userId = req.user.id;
const newItem = new Item({
name: req.body.name,
owner: userId,
});
await newItem.save((err, data) => {
if (err) {
console.log(err);
return res.status(500).json({
message: 'Server Error',
});
}
});
await newItem.populate({
path: 'owner',
model: User,
select: 'name',
});
await User.findByIdAndUpdate(
userId,
{ $push: { teams: newItem } },
{ new: true }
);
io.getIO().emit('team', {
action: 'creating',
team: newItem,
});
res.json(newItem);
};
In frontend side I am listening the socketio server with socket.io-client. In my App.js I can see data that come from backend side when I console.log(data). My app working perfect until this stage. I can take the data from socketio, I can see the new client that connected. But when I send the data with dispatch, I app start to add infinite new items to database. Take a look at my App.js
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
import { AppNavbar } from './components/AppNavbar';
import { TeamList } from './components/TeamList';
import { loadUser } from './Store/Actions/AuthActions';
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { io } from 'socket.io-client';
import { addItems } from './Store/Actions/itemActions';
function App() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(loadUser());
const socket = io('http://localhost:5000');
socket.on('team', (data) => {
if (data.action === 'creating') {
dispatch(addItems(data.team));
}
// console.log(data);
});
}, []);
return (
<div className="App">
<AppNavbar />
<TeamList />
</div>
);
}
export default App;
My itemAction in redux side is also like that
export const addItems = (input) => (dispatch, getState) => {
axios
.post('/api/items/createTeam', input, tokenConfig(getState))
.then((res) =>
dispatch({
type: ADD_ITEM,
payload: res.data,
})
)
.catch((err) =>
dispatch(
returnErrors(err.response.data, err.response.status, 'GET_ERRORS')
)
);
};
My problem is how to stop infinite callling of api after implement socketio. How to stop infinite loop efficiently?
Infinite loop is steming from not dispatching "type: ADD_ITEM" once.This issue cause that your itemAction always would dispatch "type: ADD_ITEM" and payload with it when after every fetching then your react would re-render page again and again.
You should get rid of your dispatching action inside of addItems function and dispatch your action only inside of useEffect in App.js file .
Your code snippet should look like this in App.js:
useEffect(() => {
//...
const socket = openSocket('http://localhost:5000');
socket.on('postsChannel', (data) => {
if (data.action === 'creating') {
dispatch({ type: ADD_ITEM, payload: data.team });
}
});
}, [dispatch]);

Browsers hangs while using Socket.io with ReactJS and ExpressJS

Problem:
I am working on a chat application. When I send more than 9-10 requests, the browser slows down and eventually, it just hangs. On refreshing the page, everything is back to normal.
I searched the socket.io documentation but couldn't get any solution regarding this matter.
Code:
Here is my Backend Express.JS code:
index.js
const express = require("express");
const { addUser, getUser, deleteUser, getAllUsers } = require("./users_api");
const app = express();
const server = require("http").createServer(app);
const io = require("socket.io")(server, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"],
},
});
const port = 5000 || process.env.PORT;
io.on("connection", (socket) => {
socket.on("join", ({ name, room }, callback) => {
const { err, user } = addUser({ id: socket.id, name, room });
if (err) return callback(err);
if (user) {
socket.emit("message", {
user: "admin",
text: `${user.name} has entered the room ${user.room}.`,
});
socket.broadcast.to(user.room).emit("message", {
user: "admin",
text: `${user.name} has joined the room.`,
});
socket.join(user.room);
io.to(user.room).emit("users", {
room: user.room,
users: getAllUsers(user.room),
});
callback();
}
});
socket.on("client_message", (msg) => {
const user = getUser(socket.id);
if (user) io.to(user.room).emit("message", { user: user.name, text: msg });
});
socket.on("disconnect", () => {
console.log(6);
const user = deleteUser(socket.id);
if (user) {
io.to(user.room).emit("message", {
user: "admin",
text: `${user.name} has left the room.`,
});
io.to(user.room).emit("users", {
room: user.room,
users: getAllUsers(user.room),
});
}
});
});
server.listen(port, () => console.log(`Server started at port ${port}.`));
users_api.js:
const current_users = [];
const addUser = ({ id, name, room }) => {
name = name.trim().toLowerCase().split(" ").join("_");
room = room.trim().toLowerCase();
const exist_user = current_users.find(
(user) => name === user.name && room === user.room
);
if (exist_user)
return {
err: "User with this name already exists.",
};
current_users.push({ id, name, room });
return { user: { id, name, room } };
};
const deleteUser = (id) => {
const index = current_users.findIndex((user) => user.id === id);
if (index !== -1) return current_users.splice(index, 1)[0];
};
const getUser = (id) => current_users.find((user) => user.id === id);
const getAllUsers = (room) =>
current_users.filter((user) => user.room === room);
module.exports = { addUser, deleteUser, getUser, getAllUsers };
Front-end Code:
import io from "socket.io-client";
import React, { useEffect, useRef, useState } from "react";
import queryString from "query-string";
import "./ChatPage.css";
const END_POINT = "http://localhost:5000";
const ChatPage = (props) => {
const [message, setMessage] = useState("");
const [messages, setMessages] = useState([]);
const [users, setUsers] = useState([]);
const socket = useRef(null);
const handleSubmit = () => {
socket.current.emit("client_message", message);
setMessage("");
};
useEffect(() => {
const { name, room } = queryString.parse(props.location.search);
socket.current = io(END_POINT);
socket.current.emit("join", { name, room }, (error) => {
if (error) alert(error);
});
return () => {
socket.current.disconnect();
socket.current.off();
};
}, [props.location.search, END_POINT]);
useEffect(() => {
socket.current.on("message", (msg) => {
setMessages([...messages, msg]);
});
socket.current.on("users", ({ users }) => {
setUsers(users);
});
}, [messages.length]);
return (
<div className="chat-room-container">
<div className="chat-room-left">
<ul>
{messages.map((message, i) => (
<li key={i}>
{message.name}
{message.text}
</li>
))}
</ul>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button type="button" onClick={handleSubmit}>
Submit
</button>
</div>
<div className="chat-room-right">
<ul>
{users.map((user, i) => (
<li key={i}>{user.name}</li>
))}
</ul>
</div>
</div>
);
};
export default ChatPage;
I don't understand what's wrong, please help.
Instead of sending a message on event or broadcast try to send list of messages to particular user or all users list with their messages belonging to same room. This will leads to less render in frontend.
For example,
Your Frontend code will change like,
useEffect(() => {
socket.current.on("message", (msgList) => {
setMessages(msgList);
});
socket.current.on("users", ({ users }) => {
setUsers(users);
});
}, [messages.length]);
Also if different rooms then there occurs a problem to filter the messages. So to solve this there are two possible ways:
When you send filter data from server.
Filter data in client according to user name and room name.
As this solution is not a good practice as every time the whole list of message will come and applying filter on that will increase time complexity. But you can reduce to some extent by making proper structure of data state on server.
I think this will help you.
You are creating new socket connection on every useEffect, so after ten messages, you have ten connections.
socket = io(END_POINT);
I store the created socket connection in useRef - so if it is created again, it is overwritten with the new one - and it does not duplicate.
const socketConection = useRef(null)
useEffect(() => {
socketConnection.current = io(END_POINT)
}, [deps])
Of course, in all following uses you have to use socketConection.current

fetch data with vue and sockets

I am using vuejs (CLI 3) with axios and sockets. My backend is NodeJs.
Html (view all users):
...
<ul v-if="users.length > 0">
<li v-for="user in users" :key="user.id">
<router-link :to="'/oneuser/' + user.permalink" tag="li" active-class="active" #click.native="setmyid(user._id)">
<a>{{ user.name }} - {{ user.last_name }}</a>
</router-link>
</li>
</ul>
...
<script>
import axios from 'axios'
import io from 'socket.io-client'
export default {
name: 'get-user',
data () {
return {
users: [],
socket: io('localhost:7000')
}
},
methods: {
mycall () {
axios.get('http://localhost:7000/api/users')
.then(res => {
// console.log(res)
const data = res.data
const users = []
for (let key in data) {
const user = data[key]
user.id = key
users.push(user)
}
// console.log(users)
this.users = users
})
.catch(error => console.log(error))
}
}
mounted () {
this.mycall()
this.socket.on('user-deleted', function (data) {
this.mycall()
})
}
}
</script>
Html (user view):
<script>
import axios from 'axios'
export default {
name: 'one-user',
data () {
return {
name: '',
surname: '',
id: localStorage.getItem('usId'),
socket: io('localhost:7000')
}
},
mounted () {
axios.get('http://localhost:7000/api/get-user/' + this.id)
.then(res => {
const data = res.data
this.name = data.name
this.surname = data.last_name
})
.catch(error => console.log(error))
},
methods: {
mySubmit () {
const formData = {
_id: this.id
}
axios.post('http://localhost:7000/api/delete-user', formData)
.then(this.$router.push({ name: 'get-user' }))
.catch(error => console.log(error))
}
}
}
</script>
backend NodeJs:
controller.postDeleteUser = function(req,res){
User.deleteOne({"_id" : req.body._id}, function(err){
io.emit('user-deleted', req.body._id);
res.send('ok');
});
};
When I go to user view and delete the user then it directs me to view all users. I have two major problems here.
1) After redirect, I saw again all the users even the deleted one. In my database the user has deleted correctly.
2) I don't know where exactly and how to use sockets in my code.
I am using in the front the socket.io-client npm plugin. Also I don't want to use (and I don't use it in my code) vue-socket.io because IE 11 and below version are not supported and it throws me some errors.
What I have tried so far:
1) Using watch like this:
watch: {
users: function (newValue) {
newValue = this.mycall()
}
}
This is very bad for browser performance, because always call request from the browser.
2) use beforeUpdate or Updated life-cycle:
updated () {
this.mycall()
}
That works but has bad performance. It makes requests many times to the server.
3) or that with sockets
updated () {
this.socket.on('user-deleted', function (data) {
this.mycall()
})
}
and that throws me an error:
this.mycall() is not a function
What am I doing wrong?
Where to put the code with sockets?
I have changed the view all users file to:
...
methods: {
mycall () {
axios.get('http://localhost:7000/api/users')
.then(res => {
const data = res.data
const users = []
for (let key in data) {
const user = data[key]
user.id = key
users.push(user)
}
this.users = users
})
.catch(error => console.log(error))
},
socketcall (thecall) {
this.socket.on(thecall, (data) => {
this.mycall()
})
}
}
....
created () {
this.mycall()
},
mounted () {
this.socketcall('user-deleted')
}
Life cycle-hooks cannot retrieved functions inside "this.socket.on" so I thought to do like above and it works!

Resources