Socket.io - React: How to display the users connected in a room? - javascript

I'm trying to map the users connected to my chat app and render them in a div.
I'm saving the users in an array of objects and emitting an event updating the array when the user connects and disconnects from the chat room in the backend and getting the object in the frontend, saving it in an array and using map to display the users.
When i map the array to display the users my page just shows the current user and the subsequent users, not showing the previous users connected. For example, if I open the app with the name "Bruce" and another page with the user "Andrew", the first page will show both users and the second only "Andrew". If I console.log the array I'm getting from the backend it shows both users. If I exit any page or reload it, it shows "TypeError: Cannot read property 'username' of undefined"
Backend
const users = []
// Storing user
const user = { id, username, room }
users.push(user)
const getUsersInRoom = (room) => {
return users.filter((user) => user.room === room)
}
socket.on('join', ({ username, room }, callback) => {
io.to(user.room).emit('roomData', {
room: user.room,
users: getUsersInRoom(user.room)
})
callback()
})
socket.on('disconnect', () => {
io.to(user.room).emit('roomData', {
room: user.room,
users: getUsersInRoom(user.room)
})
}
})
Frontend
const [usersName, setUsersName] = useState([]);
useEffect(() => {
socket.on('roomData', ({room, users}) => {
setUsersName(previousUsersName => [...previousUsersName, users]);
})
},[]);
return (
<div>
{
usersName.map((name, i) => (
<div key={i}>
<p>
<span>{name[i].username}</span>
</p>
</div>
))
}
<div/>
When I console.log(usersName) it shows the array in this format:
[Array(1)]
0: Array(1)
0:
id: "yXXo7TXVHWN1bzl6AAAV"
room: "Games"
username: "Bruce"
I believe the backend is correct since I'm getting all users. The problem I think is how i'm mapping the array using "name[i].username" since when I reload the page it breaks it and the page renders the names incorrectly as stated above.
Any help is appreciated, If it's confusing or I'm missing some data, please let me know.

It was the way I was displaying the names in the map using "name[i].username".
I guess because the index was updating and breaking the application, and when a new user entered the page it would not display the predecessors because the index was poiting at the new user.
I got it to work by mapping the first array containing the array of objects that I was receiving in the backend and mapping the array of objects itself to display the names.
Also the way I was saving the array in my state was wrong. Since I'm receiving all the connected users there was no need to populate the array with the previous users and then adding the new ones.
here are the changes
const [usersName, setUsersName] = useState([]);
socket.on('roomData', ({room, users}) => {
setUsersName([users]);
})
{
usersName.map((name,index) =>
<div key={index}>
{
name.map((username, subindex) =>
<p key={subindex}>{username.username}</p>
)
}
</div>
)
}

Related

Reading all the messages at once in a chat app (updating a state multiple times) React.js

I have a simple chat component, as a part of a bigger app.
After logging in, userMessages are fetched from the backend and stored in useState.
const [userMessages, setUserMessages] = useState<ChatMessage[]>([]);
const [messages, setMessages] = useState<ChatMessage[]>([]);
userMessages - all the messages addressed to the user (from other users). Based on them, unread messages are displayed.
messages - messages belonging to a given conversation (between two users) are fetched when entering a given conversation, appear in the chat window.
When a user gets a new message while not being on chat, he gets notifications about unread messages (I used socket.io).
After clicking on the blue arrow icon, the current conversation is set (based on the message property - currentConversationId) and the messages belonging to this conversation are fetched from the database.
When they appear in the chat window each received message (only the green ones) is read...
...each message.tsx component has an useEffect that sends a request to the backend to change the status of a given message from unread to read and returns this message to the frontend, then the messages are updated using useState).
# message.tsx
useEffect(() => {
!own && !read && onReadMessage?.(_id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
# communicationContext.tsx
const onReadMessage = async (id: string | undefined) => {
const updatedMessage = await CommunicationService.readMessage(id);
if (updatedMessage) {
let notification = {
receiverId: updatedMessage.sender?._id,
text: "read your message.",
silent: true,
read: false,
type: "message",
};
handleAddNotification?.(notification);
handleSendNotification?.({
...notification,
senderId: updatedMessage.sender?._id,
senderName: updatedMessage.sender?.name,
payload: { message: updatedMessage },
});
const updatedMessages = messages.map((message) =>
message._id === updatedMessage._id ? updatedMessage : message
);
setMessages(updatedMessages);
const updatedUserMessages = userMessages.map((message) =>
message._id === updatedMessage._id ? updatedMessage : message
);
setUserMessages(updatedUserMessages);
}
}
A request containing an updated message is also sent to the sender of the message via socket.io, then useState is also fired on the sender side and they see that the message has been read.
Up to this point everything works fine, but...
the problem arises when there are several unread messages at the same time.
In the database all messages are updated but the application status shows only 1-3 latest messages as read (depending on how many there are - sometimes only the last one is updated).
I know how useState works, so I expected this result, but I'm looking for a way around it and I'm out of ideas.
I need a solution that will update the entire state of the application, not just recent changes, without having to refresh the page.
I tried useReducer but got lost because there are too many useStates in communicationContext.tsx (here is a simplified version).
I suspect your onReadMessage handler should use functional state updates to eliminate race conditions or updating from stale state. It's a trivial change. The enqueued functional state updates will correctly update from the previous state versus whatever (likely stale) state value(s) are closed over in callback scope.
const onReadMessage = async (id: string | undefined) => {
const updatedMessage = await CommunicationService.readMessage(id);
if (updatedMessage) {
const notification = {
receiverId: updatedMessage.sender?._id,
text: "read your message.",
silent: true,
read: false,
type: "message",
};
handleAddNotification?.(notification);
handleSendNotification?.({
...notification,
senderId: updatedMessage.sender?._id,
senderName: updatedMessage.sender?.name,
payload: { message: updatedMessage },
});
const mapMessage = (message) => message._id === updatedMessage._id
? updatedMessage
: message;
setMessages(messages => messages.map(mapMessage));
setUserMessages(userMessages => userMessages.map(mapMessage));
}
};

Trigger won't return the proper user object in Realm/MongoDB

With Realm sync of MongoDB, I'm trying to launch a trigger when a realm user is created to insert his newly created ID into my cluster. Here's the javascript function I made that is being called by the trigger :
exports = async function createNewUserDocument({ user }) {
const users = context.services
.get("mongodb-atlas")
.db("BD")
.collection("patients");
const query = { email: context.user.data.email };
const update = {
$set: {
patientId: context.user.id
}
};
// Return the updated document instead of the original document
const options = { returnNewDocument: true };
console.log(context.user.data.email);
return users.findOneAndUpdate(query, update, options)
.then(updatedDocument => {
if(updatedDocument) {
console.log(`Successfully updated document: ${updatedDocument}.`)
} else {
console.log("No document matches the provided query.")
}
return updatedDocument
})
.catch(err => console.error(`Failed to find and update document: ${err}`))
};
When running from the embed editor, while specifying the proper user manually, it's working perfectly. However, when launched by the trigger, it looks like the user is the system user and not the created user, because the error I get in the logs is the same I get when I run from the editor by specifying System user, which is Failed to find and update document: FunctionError: cannot compare to undefined. This makes sense because the System user is not a user per se, so the context.user is undefined.
I find it weird since I specify in the function settings that it should be executed with the permissions of the user calling the function. So my question is, is it possible to access the user.context of a user on his creation, and if so, how would I do it ?

Button not changing state in react js based on api call

I have this page which shows a single post and I have a like button. if the post is liked, when the user clicks the button, it changes its state to unlike button, but if the post is not liked, then the like is getting registered and the id is getting pushed on to the array, but the button state is not getting updated and I have to reload the page to see the page. Can someone tell me how to resolve this issue?
This is the code:
const [liked, setLiked] = useState(false)
const [data, setData] = useState([]);
function likePosts(post, user) {
post.likes.push({ id: user });
setData(post);
axiosInstance.post('api/posts/' + post.slug + '/like/');
window.location.reload()
}
function unlikePosts(post, user) {
console.log('unliked the post');
data.likes = data.likes.filter(x => x.id !== user);
setData(data);
return (
axiosInstance.delete('api/posts/' + post.slug + '/like/')
)
}
For the button:
{data.likes && data.likes.find(x => x.id === user) ?
(<FavoriteRoundedIcon style={{ color: "red" }}
onClick={() => {
unlikePosts(data, user)
setLiked(() => liked === false)
}
}
/>)
: (<FavoriteBorderRoundedIcon
onClick={() => {
likePosts(data, user)
setLiked(() => liked === true)
}
}
/>)
}
Thanks and please do ask if more details are needed.
As #iz_ pointed out in the comments, your main problem is that you are directly mutating state rather than calling a setState function.
I'm renaming data to post for clarity since you have said that this is an object representing the data for one post.
const [post, setPost] = useState(initialPost);
You don't need to use liked as a state because we can already access this information from the post data by seeing if our user is in the post.likes array or not. This allows us to have a "single source of truth" and we only need to make updates in one place.
const isLiked = post.likes.some((like) => like.id === user.id);
I'm confused about the likes array. It seems like an array of objects which are just {id: number}, in which case you should just have an array of ids of the users who liked the post. But maybe there are other properties in the object (like a username or timestamp).
When designing a component for something complex like a blog post, you want to break out little pieces that you can use in other places of your app. We can define a LikeButton that shows our heart. This is a "presentation" component that doesn't handle any logic. All it needs to know is whether the post isLiked and what to do onClick.
export const LikeButton = ({ isLiked, onClick }) => {
const Icon = isLiked ? FavoriteRoundedIcon: FavoriteBorderRoundedIcon;
return (
<Icon
style={{ color: isLiked ? "red" : "gray" }}
onClick={onClick}
/>
);
};
A lot of our logic regarding liking and unliking could potentially be broken out into some sort of usePostLike hook, but I haven't fully optimized this because I don't know what your API is doing and how we should respond to the response that we get.
When a user clicks the like button we want the changes to be reflected in the UI immediately, so we call setPost and add or remove the current user from the likes array. We have to set the state with a new object, so we copy all of the post properties that are not changing with the spread operator ...post and then override the likes property with an edited version. filter() and concat() are both safe array functions which return a new copy of the array.
We also need to call the API to post the changes. You are using the same url in both the "like" and "unlike" scenarios, so instead of calling axios.post and axios.delete, we can call the generalized function axios.request and pass the method name 'post' or 'delete' as an argument to the config object. [axios docs] We could probably combine our two setPost calls in a similar way and change likePost() and unlikePost() into one toggleLikePost() function. But for now, here's what I've got:
export const Post = ({ initialPost, user }) => {
const [post, setPost] = useState(initialPost);
const isLiked = post.likes.some((like) => like.id === user.id);
function likePost() {
console.log("liked the post");
// immediately update local state to reflect changes
setPost({
...post,
likes: post.likes.concat({ id: user.id })
});
// push changes to API
apiUpdateLike("post");
}
function unlikePost() {
console.log("unliked the post");
// immediately update local state to reflect changes
setPost({
...post,
likes: post.likes.filter((like) => like.id !== user.id)
});
// push changes to API
apiUpdateLike("delete");
}
// generalize like and unlike actions by passing method name 'post' or 'delete'
async function apiUpdateLike(method) {
try {
// send request to API
await axiosInstance.request("api/posts/" + post.slug + "/like/", { method });
// handle API response somehow, but not with window.location.reload()
} catch (e) {
console.log(e);
}
}
function onClickLike() {
if (isLiked) {
unlikePost();
} else {
likePost();
}
}
return (
<div>
<h2>{post.title}</h2>
<div>{post.likes.length} Likes</div>
<LikeButton onClick={onClickLike} isLiked={isLiked} />
</div>
);
};
CodeSandbox Link

MERN Stack: How to prevent a User from registering (being added to an array) multiple times?

Semi-new developer building a project using the MERN stack.
The app has two models, one for Users and one for Tournaments. Tournament model has an attribute called participants which is an array.
I wrote an Express backend route so that a User can register for Tournaments.participants[].
This looks like:
router.post('/:id', (req, res) => {
Tournament.findById(req.params.id)
.then(tournament => {
tournament.participants.push(req.body);
return tournament.save();
})
.then(savedTournament => res.json(savedTournament))
.catch(err => res.json(err));
});
However, a User can just keep clicking Sign Up and I'd have a bunch of duplicate users, so I'm trying to write a conditional that will disable Sign Up if the user is already in Tournament.participants[].
I tried writing a conditional inside the Express route using Array.includes(req.body) but couldn't hack it.
Looked something like
Tournament.findById(req.params.id)
.then(tournament => {
if (tournament.participants.includes(req.body) {
return res.status(400).json({ msg: "This user already signed up for this tournament" });
} else {
tournament.participants.push(req.body);
return tournament.save();
}
})
.then(savedTournament => res.json(savedTournament))
.catch(err => res.json(err));
I tried different variations as well, like if (tournament.participants.includes(!req.body)) then push(req.body), etc.
And I also tried just rendering a different button if the participants.includes(user) but I believe this should be done on the backend anyway.. I'm open to suggestions.
Can anyone help me out?
In general, you can't use the native comparison operators with objects, includes included:
const foo = { id: 1 };
const bar = [{ id: 1 }];
console.log(bar.includes(foo)); // outputs `false`
You should use some kind of item id in order to check if its already exists:
function isIdIncluded(arr, id) {
return arr.some(x => x.id === id)
}
const foo = { id: 1 };
const bar = [{ id: 1 }];
console.log(isIdIncluded(bar, 1)); // outputs `true`
I assume you are keeping the users's _id in the participants array, and your tournament schema is similar to this:
const tournamentSchema = new mongoose.Schema({
name: String,
participants: Array,
});
Now if you send a request with this body:
{
"user": "5e97255a342f395774f30162" //the user id who wants to participate
}
You can use this code (I just changed the req.body to req.body.user)
Tournament.findById(req.params.id)
.then((tournament) => {
if (tournament.participants.includes(req.body.user)) {
return res.status(400).json({ msg: "This user already signed up for this tournament" });
} else {
tournament.participants.push(req.body.user);
return tournament.save();
}
})
.then((savedTournament) => res.json(savedTournament))
.catch((err) => res.status(500).json(err));
Now when a user first time participates a tournament, the document will be like this:
{
"participants": [
"5e97255a342f395774f30162"
],
"_id": "5e97255a342f395774f30161",
"name": "Chess Tournament"
}
And when the same user tries, the response will be like this with a 400 status code.
{
"msg": "This user already signed up for this tournament"
}
Also please note that, the user id shouldn't be send in the request body, but it must be the user's id who is logged in.

How to fetch and populate data from firebase collections in react correctly?

So i have component called Agents, that's by it's nature is table with rows that contain data about agents.
I want to get full data about agents from firestore, populate it with data from another collection in firestore, and then render it to the screen.
Here is working piece of code, but i have some questions:
Why now i have render with some delay? (First i have spinner about loading - it's okay, i expect it, but then spinner hides and i see 1 agent in table for 0.5s, and then i see all data. I want to see all data after spinner gone. What is the problem of my code?
How to populate data. Maybe there is another way to do that, or maybe there is another good practice, cause i think my "componentDidMount" is horrible, the solutions that i use are not common i guess.
So for 2 question i will describe what i want:
In users collection i have documents, they are objects, that have property called - profile. That property contains ID of document from collection called roles! I want to get that data by that document id. That's why my componendDidMount is async, cause i want to wait till i get data from roles collection by id of document.
Help me please optimise my code )
class Agents extends Component {
state = {
isLoading: true,
agents: [],
showOptionsFor: null
}
async componentDidMount() {
firebase
.firestore()
.collection('users')
.orderBy('lastName')
.onSnapshot(async snapshot => {
let changes = snapshot.docChanges()
let agents = this.state.agents
for (const change of changes) {
const agent = {
id: change.doc.id,
...change.doc.data()
}
if (change.type === 'added' || change.type === 'modified') {
await firebase
.firestore()
.collection('roles')
.doc(change.doc.data().profile).get().then( response => {
agent['role'] = response.data().profile
agents.push(agent)
}
)
}
if (change.type === 'modified') {
const newAgentsArray = agents.filter(element => {
return element.id !== change.doc.id
})
agents = newAgentsArray
agents.push(agent)
}
}
this.setState({
agents: agents,
isLoading: false
})
})
}

Categories