I am having two users have a private message by having both of them join the same Socket.io room, which is based on the room created from the sender and receiver ids. Every time I create a message, the client rejoins the room, due to a Socket.io emit event re-rendering because of a state change, and the roomId is console logged. As a result, the message is not being transmitted over multiple instances of the chat when it has joined the same room.
const ChatApp = () => {
const [messages, setMessages] = React.useState([]);
const userId = useSelector(state => state.profile.profile._id);
const senderFullName = useSelector(state => state.auth.user.fullname);
const authId = useSelector(state => state.auth.user._id);
const roomId = [authId, userId].sort().join('-');
const ioClient = io.connect("http://localhost:5000");
ioClient.emit('join', {roomid: roomId});
ioClient.emit('connected', {roomid: roomId} );
ioClient.on('message', (msg) => {
const messageObject = {
username: msg.from,
message : msg.body,
timestamp: msg.timestamp
};
addMessage(messageObject);
});
const addMessage = (message) => {
const messagess = [...messages];
messagess.push(message);
setMessages(messagess);
console.log(messagess);
}
const sendHandler = (message) => {
var res = moment().format('MM/DD/YYYY h:mm a').toString();
// Emit the message to the server
ioClient.emit('server:message', {from: senderFullName, body: message, timestamp: res });
}
return (
<div className="landing">
<Container>
<Row className="mt-5">
<Col md={{ span: 8, offset: 2 }}>
<Card style={{ height: "36rem" }} border="dark">
<Messages msgs={messages} />
<Card.Footer>
<ChatInput onSend={sendHandler}>
</ChatInput>
</Card.Footer>
</Card>
</Col>
</Row>
</Container>
</div>
)
};
Api code that is used in server
io.sockets.on("connection",function(socket){
// Everytime a client logs in, display a connected message
console.log("Server-Client Connected!");
socket.on('join', function(data) {
socket.join(data.roomid, () => {
//this room id is being console logged every time i send a message
console.log(data.roomid);
});
});
socket.on('connected', function(data) {
//load all messages
console.log('message received');
(async () => {
try {
console.log('searching for Schema');
const conversation = await Conversation.find({roomId: data.roomid}).populate('messages').lean().exec();
const mess = conversation.map();
console.log(mess);
console.log('Schema found');
}
catch (error) {
console.log('Schema being created');
Conversation.create({roomId: data.roomid});
}
})();
});
socket.on('server:message', data => {
socket.emit('message', {from: data.from, body: data.body, timestamp: data.timestamp});
console.log(data.body);
Message.create({from: data.from, body: data.body});
})
});
You could either move the socket state up to a parent component, or move the event initialization into a custom hook that is shared once across your application, or into a useEffect
hook here so it only runs on initial render.
Probably the simplest is the latter option, you can just wrap those lines of code you only want to be written once up like this:
useEffect(() => {
ioClient.emit('join', {roomid: roomId});
ioClient.emit('connected', {roomid: roomId} );
ioClient.on('message', (msg) => {
const messageObject = {
username: msg.from,
message : msg.body,
timestamp: msg.timestamp
};
addMessage(messageObject);
});
}, []);
The empty array at the end signifies that this will only run on initial render, and not again, as it has no dependencies.
Update
You may or may not be having closure or timing issues, but to be on the safe side, replace the line "addMessage(messageObject);`" with:
setMessages((previousMessages) => [messageObject, ...previousMessages]);
This will do the same thing as the addMessage
function, but by passing a callback function to setMessages
, you avoid using the state object from outside.