Socket.io emit event only works once/first time - node.js

I'm brand new to socket.io and am trying to create an app similar to slido - users can send in messages and then view all messages being submitted in real time. I'm using node.js, express, socket.io, and redis in the back end. React and socket.io-client in front end.
At the moment, the live messages page/feed only updates (in real time) the first time a message is sent in, after that the emit even appears to stop working and the list of messages will only update when you refresh the page and it pulls the message history from redis.
Does anyone know why this may be happening? I've checked that the versions of socket.io for server and client are the same.
Thank you!
server-side socket setup:
io.on("connect", (socket) => {
initialiseUser(socket);
socket.on("dm", (message) => {
dm(socket, message, io);
});
io.emit("hello", "hello world");
socket.on("disconnecting", () => onDisconnect(socket));
});
// dm logic sits in separate file
module.exports.dm = async (socket, message, io) => {
message.from = socket.user.userid;
const messageString = [message.from, message.content].join(".");
await redisClient.lpush(`prayers:messages`, messageString);
io.emit("dm", message);
};
client-side setup:
const useSocketSetup = (setMessages, messages) => {
const { setUser } = useContext(AccountContext);
useEffect(() => {
socket.connect();
socket.on("hello", (content) => {
console.log("hello world", content);
});
socket.on("messages", (redisMessages) => {
setMessages(redisMessages);
});
socket.on("dm", (message) => {
setMessages((prevMessages) => [message, ...prevMessages]);
console.log("NEW MESSAGE", message);
});
socket.on("connect_error", () => {
console.log("Socket cannot connect");
setUser({ loggedIn: false });
});
return () => {
socket.off("connect_error");
socket.off("messages");
socket.off("dm");
};
}, [setUser, setMessages, messages]);
};
export default useSocketSetup;
The console log sitting inside socket.on("dm".... is only being logged on the first dm event.
This is the form setup for submitting a message:
const { setMessages } = useContext(MessagesContext);
useSocketSetup(setMessages);
return (
<>
<Formik
initialValues={{ message: "" }}
validationSchema={Yup.object({ message: Yup.string().min(1).max(255) })}
onSubmit={(values, actions) => {
const message = { from: null, content: values.message };
socket.emit("dm", message);
setMessages((prevMessages) => [message, ...prevMessages]);
console.log(JSON.stringify(message));
actions.resetForm();
navigate("/prayers");
}}
>
There is then a component accessing the messages from context and mapping through them to display.

Related

socket emits does not do anything, no messages are reflected in ReactsJs UI

i am a newbie in socket.io, i apologize for any bad practices or outright errors in code below, any help would be really appreciated.
i am trying to make a simple chats app where two people can join a room and chat with each other using Socket.io and Reactjs + Nodejs
my server file is as below
io.on("connect", (socket) => {
console.log(socket.id, " connected"); //prints
socket.on("disconnect", () => {
console.log(socket.id, " Disconnected"); //prints
});
socket.on("join", ({ room, username }) => {
console.log(`${username} joined room ${room}`) //prints
socket.on("message", (payload) => {
console.log(`${username} says - ${payload.message}`); //prints
socket.emit("message", payload) //does not reflect in front-end
})
socket
.to(room)
.emit("message", {
message: `${username} has joined the Room`,
isToast: true,
}); //does not reflect on front-end
socket.emit("message", {
message: `Hello ${username}, Welcome to Room ${room}`,
isToast: true,
}); //does not reflect on front-end
});
});
my frontend is as following
const Home = ({ socket }) => {
const msgRef = useRef();
const { setSession } = useSessionContext();
const [msg, setMsg] = useState([]);
socket.on("message", (payload) => {
// setMsg(prev => [...prev, payload]); //remains empty for some reason
console.log(payload); //prints correctly only when user himself sends, texts from others dont reflect
});
const handleMsgSend = (e) => {
e.preventDefault();
const newText = msgRef.current.value;
socket.emit("message", { message: newText, isToast: false })
e.target.reset();
};
.
.
.
.
thankyou for your time, i have been banging my head on this since days, rage quitting and trying again in the verge of tears.
Expected behavior :-
user connects - xyz connected (log)
on room join - xyz joined room abc (log)
xyz sends text - xyz says qwerty (log)
text gets emited to other connected user including sender via message
xys has joined room get emited to all users in room except the sender via message
hello xyz welcome to room abc gets emited to only sender via message

How to set socket attribute globally using socket.io in nodejs from within a socket event handler?

Currently working on a portfolio project, and running into a problem where I am trying to set an attribute for the socket variable globally from within a local socket event handler so that it can be accessed when handling other socket events.
These are the events I'm handling: a login event and a disconnect event.
io.on("connect", socket => {
console.log(`User joined: `, socket.id)
socket.on("login", (data) => handleUserLogin(socket, data))
socket.on("disconnect", () => handleDisconnect(socket))
})
When the user logs on, I emit a login event from the client and the login event handler takes in data of a JSON object with user details, and a company ID, both sent from the client. I'm trying to save this companyId gloablly. This companyId attribute is supposed to help determine which list to append/collect etc.
const handleUserLogin = async (socket, data) => {
const { companyId, user } = data;
socket.join([`${socket.id}`, `${companyId}`]);
socket.companyId = companyId;
try {
const newOnlineUser = await redisClient.hset(`${companyId}:users:online`, `${socket.id}`, JSON.stringify(user))
if (newOnlineUser) {
const onlineUsers = await redisClient.hgetall(`${companyId}:users:online`)
socket.to(companyId).emit("user_status_change", { onlineUsers })
}
} catch (error) {
socket.to(`${socket.id}`).emit("error", { error })
}
};
When the socket disconnects, I want to remove the user from my redis list, which means I'll need this companyId attribute. But a value of null appears when I try to access: socket.companyId.
const handleDisconnect = async (socket) => {
console.log(`Disconnect: ${socket.companyId}`)
if (socket?.companyId) {
console.log('Disconnect event for user', socket.id, 'of company', socket.companyId, 'occurred.' )
try {
const offlineUser = await redisClient.hdel(`${socket.companyId}:users:online`, `${socket.id}`)
if (offlineUser) {
const onlineUsers = await redisClient.hgetall(`${companyId}:users:online`)
socket.to(companyId).emit("user_status_change", { onlineUsers })
}
} catch (error) {
console.log(error)
}
}
}
Would love to know how to deal with this, or at least find a way to set an attribute to the socket instance from within event handling, for which can be accessed also when handling other events.

Socket.io and Redis Adapter same rooms and different servers

We are creating a scalable real-time collaborative text editor. The client (frontend) code is written in React.js, and the server (backend) code is Websockets (socket.io).
The client creates a connection with the websocket server and joins a room based on the documentID opened in the URL, so that each event is done inside the same document will be shared across all clients opening the same document since they are joining a room with the same documentID
client code
useEffect(() => {
if (socket == null || quill == null) return
//Emit save-document event every 500ms
const interval = setInterval(() => {
socket.emit("save-document", quill.getContents())
}, SAVE_INTERVAL_MS)
return () => {
clearInterval(interval)
}
}, [socket, quill])
useEffect(() => {
if (socket == null || quill == null) return
socket.once("load-document", document => {
quill.setContents(document)
quill.enable()
})
socket.emit("get-document", documentId)
}, [socket, quill, documentId])
useEffect(() => {
if (socket == null || quill == null) return
const handler = delta => {
quill.updateContents(delta)
}
socket.on("receive-changes", handler)
return () => {
socket.off("receive-changes", handler)
}
}, [socket, quill])
server code
socket.on('get-document', async (documentID) => {
const document = await lookUpDocument(documentID);
socket.join(documentID);
socket.emit("load-document", document.data);
socket.on("send-changes", (delta) => {
socket.broadcast.to(documentID).emit("receive-changes", delta)
})
socket.on("save-document", async (data) => {
await Document.findByIdAndUpdate(documentID, { data })
})
It works perfectly if we are working on the same server, but we want to have several Websockets servers that are connected communicate with each other as well. For example
Client1 connected to WS1.
Client2 and Client3 connected to WS2.
The three Clients are opening the same document, so that each event emitted by a client must be broadcasted to all the other clients even if they are connected to different Websockets. It should look like this
After searching I found out that we should consider Publish/Subscribe Architecture so that servers can subscribe and publish events. We also found that Socket.io has Redis-Adapter which does the same thing. So we modified our server code be like this
const io = new Server(process.env.PORT, {
cors: {
origin: process.env.CLIENT_URL,
methods: ["GET", "POST"]
}
})
const pubClient = createClient({ host:'https://<ngrokurl>.eu.ngrok.io'});
const subClient = pubClient.duplicate();
pubClient.on('ready', () => {
console.log('Publisher connected to redis and ready to use')
})
subClient.on('ready', () => {
console.log('Subscriber connected to redis and ready to use')
})
pubClient.on('error', (err) => console.log('Publisher Client Error', err));
subClient.on('error', (err) => console.log('Subscriber Client Error', err));
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
//Connecting the socket server to the redis channel
//using Socket.io Redis-Adapter
io.adapter(createAdapter(pubClient, subClient));
});
const defaultValue = ""
var allClients = [];
io.on("connection", (socket) => {
allClients.push(socket)
var username = socket.handshake.query.username
console.log(`A client is connected! ${username} - Number of sockets is: ${allClients.length}`)
//Event listener for client's socket disconnect
//Event that listens to any
socket.on('disconnect', function (reason) {
console.log(`${username} got disconnected due to ${reason}`)
var i = allClients.indexOf(socket);
allClients.splice(i, 1);
console.log(`Number of sockets now is: ${allClients.length}`)
})
socket.on('get-document', async (documentID) => {
const document = await lookUpDocument(documentID);
socket.join(documentID);
socket.emit("load-document", document.data);
socket.on("send-changes", (delta) => {
socket.broadcast.to(documentID).emit("receive-changes", delta)
})
socket.on("save-document", async (data) => {
await Document.findByIdAndUpdate(documentID, { data })
})
})
})
Yet each server now doesn't broadcast the events to all the websocket servers whenever the document changes? How can I make each socket subscribe and publish events to all the clients that are joining the same room (based on the documentID)?
Thank you so much in advance
Edit 1: Added the redis url that we connect to which is an ngrok server that tunnels to a localhost:6379 on a virtual machine.

How to listen to socketIO private message in React client?

I have a SocketIO instance in an Express app, that listens to a React client requests. A user can send private messages to a specific person. The server receives the private message, and should dispatch it back to both sender & recipient thanks to the io.to(socketId).emit(content) method.
How to listen to this event in React and update the message array? In order to ease the process, I have created a connectedUsers object, whose keys are mongoDB's user._id, and whose values are the unique socketID generated by socketIO. This way, I can easily address message to specific persons in the client. Once sent, the messages are stored in a MongoDB database.
Here is the back-end. The point of interest is io.on("privateMessage")
const connectedUsers = {};
const socketManager = (io) => {
io.on("identifyUser", (user) => {
if (!([user.id] in connectedUsers)) {
connectedUsers[user.id] = io.id;
}
});
io.on("privateMessage", (data) => {
io.to(connectedUsers[data.recipientId]).emit(data.message);
io.to(connectedUsers[data.senderId]).emit(data.message);
});
io.on("disconnect", () => console.log("user disconnected!"));
};
Here is the listening function in React. Everything works but the "privateMessage" part.
async function getUser(socketId) {
try {
const res = await ax.get(`${serverUrl}/login`);
const socket = io(serverUrl);
socketId.current = socket;
socket.on("connect", () => {
socket.emit("identifyUser", { id: res.data._id });
socket.on("privateMessage", (data) =>
console.log("private message received!", data)
);
});
} catch (err) {
throw new Error(err);
}
}
Thanks for your help!
I think you need to put the socket.on("privateMessage") part outside the socket.on("connect") scope.
React must load all events at the beginning.
The backend side must be responsible for the authorization.
For the client there is connection event, not connect.
Subscription to event privateMessage should be outside connection callback.
This code should work. Hope this helps
import io from 'socket.io-client'
async function getUser(socketId) {
try {
const res = await ax.get(`${serverUrl}/login`);
const socket = io(serverUrl);
socketId.current = socket;
socket.on("connection", () => {
socket.emit("identifyUser", { id: res.data._id });
});
socket.on("privateMessage", (data) =>
console.log("private message received!", data)
);
} catch (err) {
throw new Error(err);
}
}

Socket.io - React : How to retrieve saved socket.io data (MongoDB) in React?

I followed few tutorials on how to display messages sent in the chatroom before joining in, but I don't know how to display them in React and I have few questions below in the server side.
Client side, in constructor :
this.state = {
msg: "",
messages: []
};
Client side, I have a form which clicked button will send the message to the server by this function :
sendMessage(e) {
e.preventDefault();
let msg = this.state.msg;
this.socket.emit("sendMessage", msg);
this.setState({ msg: "" });
}
Server side, I have a mongoose Schema for the message, named Message and the collection in the database is messages.
const Message = new mongoose.Schema({
sender: {
type: "string"
},
message: {
type: "string"
}
});
var messages = [];
io.on("connection", (socket, user) => {
var user = socket.request.session.user;
Message.find({}).exec((err, messages) => {
if (err) throw err;
console.log(messages);
io.emit("showingPastMessages", messages);
});
console.log(messages) shows in PowerShell all the messages (entries) saved in Mongo in an array of javascript objects ?
[{id_ : 4qxxx, sender : 'user123', message : 'hello!'}, {id_ : 5exxx, sender : 'user456', message : 'hi!'}]
I would like to know if it is possible to access only to sender and message properties to send it to the client ? Something like messages.sender + messages.message because when I console.log(messages.message) it shows undefined
Here is where the server receives the message sent then saves it in Mongo.
socket.on("sendMessage", function(msg) {
var newMsg = new Message({ message: msg, sender: user });
newMsg.save(function(err) {
if (err) {
console.log(err);
} else {
messages.push(newMsg);
console.log(newMsg);
console.log(messages);
}
});
});
console.log(newMsg) shows the latest msg sent, but the console.log(messages) doesn't show the previous messages but only the latest one, why ?
then in React, I should have something like this in constructor, in ComponentDidMount() ? If should it be with prevState
this.socket.on("showingPastMessages", function(messages){
this.setState({ ...this.state.messages, messages})
});
Could you could give me some advices ?
Here my client side code to retrieve the data:
class Chat extends Component {
constructor(props) {
super(props);
this.state = {
msg: "",
messages: []
};
var socket = io('http://localhost:3000');
this.socket.on("history", function(messages) {
console.log(messages);
});
}
That's a good start. What I would suggest doing is making this a little more event driven. To do that, you'll want to add the sockets to a room when they connect. How many rooms and how to split up the rooms will depend on your app, but I'll demonstrate the basic idea.
First, when a socket connects to your server, add that socket to a room and emit your chat history immediately.
io.on('connection', async (socket) => {
socket.user = socket.request.session.user;
socket.join('chat');
try {
const messages = await Message.find({});
socket.emit('history', messages);
} catch (err) {
// Handle this error properly.
console.error(err);
}
});
Then, later on, when you receive a message, you'll want to save that message and emit it to all of the sockets in your chat room.
socket.on("sendMessage", (msg, callback) => {
const message = new Message({ message: msg, sender: socket.user });
message.save(function(err) {
if (err) {
return callback(err);
}
io.to('chat').emit('message', message);
});
});
Finally, on the client side, you'll want to listen for the history event. When you receive it, you'll want to clear the chat history you currently have and replace it with what the server is telling you. Maybe this would look something like
socket.on('history', (messages) => {
this.setState({ messages });
});
You'll also want to listen for this message event, but with this event, you'll only append the message to your history. This might look something like
socket.on('message', (message) => {
this.setState({ messages: [ ...messages, message ] });
});
A word of warning, if when you tell the server about a new message, do not add it to your messages state array until you receive the message event. If you do so, you will notice double messages. For example, this might look something like
onSendMessage(evnt) {
evnt.preventDefault();
socket.emit("sendMessage", msg);
this.setState({ msg: "" });
}
Note: After receiving some feedback from the OP, I wanted to add a section on where to put the event handlers attached to the socket (i.e. all the socket.ons). My suggestion would be to add this code in the file that defines the Message schema at the bottom of the file in the io.on('connection') callback. For example,
const Message = new mongoose.Schema({/*...*/});
io.on('connection', (socket) => {
// Everything else I wrote above...
socket.on('sendMessage', (msg, callback) => {
// ...
});
});
On the client side, the event handlers would probably be registered when the chat component is mounted. For example,
class ChatComponent extends React.Component {
componentDidMount() {
this.socket = io('https://your-server-or-localhost/');
this.socket.on('history', (messages) => {
// See above...
});
}
}

Resources