React Native, NodeJS, Socket.io - node.js

i am trying to implement a 1-1 private messaging feature inside my app, where 2 users can exchange messages live if they are both on the chat screen at the same time and if not the messages get stored in my postgres database and when the user opens the chat again they are loaded.
Currently with my code, when both users chat are open, when i try sending a message, the message does not get sent live i need to refresh the app in order for the chat to update. I think my socket is working since my console logs are getting returned in both frontend and backend consoles.
my problem is how to make the messages live and update my flatlist correctly(ie. the new messages appear at the bottom of my inverted list)?
Here is my code:
Client Side
const message = route.params.message;
const [messages, setMessages] = useState([]);
const [text, setText] = useState('');
const [socket, setSocket] = useState(null);
const { user } = useAuth();
useEffect(() => {
const newsocket =io.connect(socketURL)
setMessages(message.Messages)
newsocket.on('connect', msg => {
console.log(`user: ${user.id} has joined conversation ${message.id}`)
setSocket(newsocket)
});
newsocket.on("send_message", (msg) => {
console.log("this is the chat message:", msg);
const data = [...messages];
data.push(msg);
setMessages(data);
});
return(()=>newsocket.close());
}, []);
const onSend = (ConversationId,senderId,receiverId,message) => {
messagesApi.sendMessage({ConversationId,senderId,receiverId,message});
setText("")
const to = (user.id===route.params.message.user1?
route.params.message.user2:route.params.message.user1)
socket.emit(
'message', { to: to, from: user.id, message,ConversationId });
};
const updateText=(text)=>{
setText(text);
}
<FlatList
inverted
data={messages}
keyExtractor={(item,index)=>index.toString()}
extraData={messages}
renderItem={({item,index})=>(
<>
<Text>
{moment(item.createdAt).fromNow()}
</Text>
<MessageBubble
text={item.message}
mine={item.senderId !== user.id}
/>
</>
)}
bounces={false}
/>
<View style={styles.messageBoxContainer}>
<TextInput
onChangeText={updateText}
value={text}
/>
<TouchableOpacity
onPress={()=>{
onSend(
message.id,
user.id,
(user.id===message.user1?message.user2:message.user1),
text
)}}
>
<Text>Send</Text>
</TouchableOpacity>
</View>
Server Side
const express = require("express");
const app = express();
const http = require("http");
const socket = require("socket.io")
const server=http.createServer(app);
const io =socket(server)
io.on('connection', (socket) => {
console.log("connected")
socket.on('message', (data) => {
console.log(data)
socket.emit('send_message', { message: data.message, receiverId:
data.to,senderId:data.from,conversationId:data.ConversationId })
});
});
Thank you in advance, i have been trying to solve this problem for weeks and couldnt. Would really appreciate any help.

after many attempts i managed to get it working thanks to Srikanth's answer on this Creating a private chat between a key using a node.js and socket.io.
CLIENT
useEffect(() => {
const newsocket =io.connect("http://192.168.1.103:9000")
setMessages(message.Messages)
newsocket.on('connect', msg => {
console.log(`user: ${user.id} has joined conversation ${message.id}`)
setSocket(newsocket)
newsocket.emit('subscribe', message.id);
});
newsocket.on("send_message", (msg) => {
console.log("this is the chat messages:", msg);
setMessages(messages => messages.concat(msg))
});
return(()=>newsocket.close());
}, []);
const onSend = (ConversationId,senderId,receiverId,message) => {
console.log("sent")
const to = (user.id===route.params.message.user1?
route.params.message.user2:route.params.message.user1)
socket.emit('message', { to: to, from: user.id, message,ConversationId });
setText("")
messagesApi.sendMessage({ConversationId,senderId,receiverId,message});
};
SERVER
io.on('connection',(socket)=>{
console.log('User '+socket.id+' connected')
socket.on('subscribe', (room)=> {
console.log('joining room', room);
socket.join(room);
});
socket.on('message', (data) => {
console.log(data)
console.log('sending room post',data.ConversationId)
io.sockets.in(data.ConversationId).emit('send_message', { message:
data.message, receiverId:
data.to,senderId:data.from,conversationId:data.ConversationId });
})
})
I have read many comments that suggested not to use rooms in 1-1 private messaging but this was the only possible way of getting it to work.

Related

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

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.

socket.io insufficient resources - react js and node

I'm experimenting a bit with socket.io to create a chat app in real time.
Everything seems to work fine but after a few second socket.io gives me this error:
"websocket.js:50 WebSocket connection to 'ws://localhost:5000/socket.io/?EIO=4&transport=websocket&sid=gbx90aAo73oDoP86AAVP' failed: Insufficient resources"
What am I doing wrong?
Client
import React from "react";
import { useState, useEffect } from "react";
import { io } from "socket.io-client";
const App = () => {
const [conv, setConv] = useState([]);
const [input, setInput] = useState({
sender: "user",
text: "",
});
const socket = io("http://localhost:5000");
const handleChange = (e) => {
setInput({
sender: "user",
text: e.target.value,
});
};
const handleSubmit = (e) => {
e.preventDefault();
socket.emit("input", input);
};
useEffect(() => {
socket.on("output", (data) => {
setConv(data[0].messages);
});
}, [conv]);
return (
<div className="App">
<form onSubmit={handleSubmit}>
<h1>chat</h1>
<div id="messages">
{conv.map((msg, index) => (
<div key={index}>
<p>
{msg.sender}: {msg.text}
</p>
</div>
))}
</div>
<input onChange={handleChange} type="text" />
<input type="submit" />
</form>
</div>
);
};
export default App;
Server
const express = require("express");
const socketIo = require("socket.io");
const http = require("http");
const PORT = 5000;
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: "http://localhost:3000",
},
}); //in case server and client run on different urls
const MongoClient = require("mongodb").MongoClient;
MongoClient.connect("mongodb://localhost:27017", (err, db) => {
if (err) throw err;
console.log("connected");
const database = db.db("local");
const chat = database.collection("chat");
//connect to socket .io
io.on("connection", (socket) => {
// get chat from collection
chat.find().toArray((err, res) => {
if (err) throw err;
// emit the messages
socket.emit("output", res);
});
//handle input events
socket.on("input", (input) => {
let sender = input.sender;
let receiver = "bot";
let text = input.text;
// check if inside chat collection the members array contains the sender and receiver
chat.find({ members: { $all: [sender, receiver] } }).toArray((err, res) => {
if (err) throw err;
let senderInDb = res[0].members[0];
let receiverInDb = res[0].members[1];
//if yes then push the message to the messages array
if (sender === senderInDb && receiver === receiverInDb) {
chat.updateOne(
{ members: { $all: [sender, receiver] } },
{ $push: { messages: { sender: sender, text: text } } }
);
}
});
});
});
});
server.listen(PORT, (err) => {
if (err) console.log(err);
console.log("Server running on Port ", PORT);
});

message sent but not received socket.on(...) for client

(Working) I can see the message was sent to the server just fine:
// function used in reactjs(client)
const sendMessage = () => {
const data = {
room: room,
content: {
author: userName,
message: message
}
}
Promise.resolve(socket.emit('send_message', data))
.then(() => setMessage(''));
}
useEffect(() => {
socket.on('receive_message', data => {
console.log(data);
});
}, []);
// function used in index.js(server)
io.on('connection', (socket) => {
console.log(socket.id);
socket.on('join_room', (roomName) => {
socket.join(roomName); // creates room with the name
console.log('User Joined Room:', roomName);
});
socket.on('send_message', (data) => {
console.log("data =", data);
socket.to(data.room).emit('receive_message', data.content);
});
socket.on('disconnect', () => {
console.log('USER DISCONNECTED')
});
});
(Problem) Can anyone tell me why receive_message is not showing up in the console.log?
Note: it will only show if I do something like add some comment to the react file and save it.(weird)
The Full Code of both files
index.js(server)
const express = require('express');
const morgan = require('morgan');
const { createServer } = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
app.use(morgan('dev'));
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: true,
origins: ['http://localhost:3000']
});
io.on('connection', (socket) => {
console.log(socket.id);
socket.on('join_room', (roomName) => {
socket.join(roomName); // creates room with the name
console.log('User Joined Room:', roomName);
});
socket.on('send_message', (data) => {
console.log("data =", data);
socket.to(data.room).emit('receive_message', data.content);
});
socket.on('disconnect', () => {
console.log('USER DISCONNECTED')
});
});
httpServer.listen(3002, () => console.log(`Socket.io test running on port 3002`))
app.js(client)
import React, { useEffect, useState } from 'react';
import io from 'socket.io-client';
import './App.css';
let socket = io('localhost:3002/');
function App() {
// variables
const [loggedIn, setLoggedIn] = useState(false);
const [room, setRoom] = useState('');
const [userName, setUserName] = useState('');
const [message, setMessage] = useState('');
const [messageList, setMessageList] = useState([]);
// functions
const connectToRoom = () => {
console.log('button clicked');
setLoggedIn(true);
socket.emit('join_room', room); // join_room copied from backend
}
const sendMessage = () => {
const data = {
room: room,
content: {
author: userName,
message: message
}
}
Promise.resolve(socket.emit('send_message', data))
.then(() => setMessage(''));
}
// setup
useEffect(() => {
socket.on('receive_message', data => {
console.log(data);
});
}, []);
return (
<div className="App">
{!loggedIn ?
<div className="logIn">
<div className="inputs">
<input type="text" placeholder="Name..." value={userName} onChange={ev => setUserName(ev.target.value)} />
<input type="text" placeholder="Room..." value={room} onChange={ev => setRoom(ev.target.value)} />
</div>
<button onClick={connectToRoom}>Enter Chat</button>
</div> :
<div className="chatContainer">
<div className="messages">
<h1>{room}</h1>
{messageList.map((messageInfo, index) =>
<div key={index} className="messageBox">
<p>{messageInfo.author}: {messageInfo.message}</p>
</div>
)}
</div>
<div className="messageInputs">
<input type="text" placeholder="Message..." value={message} onChange={ev => setMessage(ev.target.value)} />
<button onClick={sendMessage}>Send</button>
</div>
</div>
}
</div>
);
}
export default App;
I found the issue. The state is managed in two different places on the client side.
For the socket.on('receive_message', ...) in useEffect, this will show up on every single other browser console logged into the current room except the current browser's console(the browser sending the message).
To also get it to show in current console browser window, I also needed to add the state in where I am submitting it.
const sendMessage = () => {
const data = {
room: room,
content: {
author: userName,
message: message
}
}
Promise.resolve(socket.emit('send_message', data))
.then(() => console.log(data)) // <------- add this line here
.then(() => setMessage(''));
}
This of course means setMessageList will need to be called both inside the sendMessage() and the socket.on('receive_message',...)(within the useEffect())
However, be careful when calling setMessageList inside socket.on('receive_message',...). The reasons for this is because it is called within a single useEffect one time, it captures the state from when that function was called and will not update. In other words, you cannot use setMessageList([...messageList, data]) because this function will only read messageList from when this function was first called initially.
To get around this, I see two options...
You can remove the [] inside the useEffect((...), []) forcing it to run in an infinite loop (this seems expensive).
The other option is to have the data on the backend update, save, then send back to which the setMessageList(data)(inside socket.on('receive_message',...)) will have to reload all the messages and re-render them all. (still seems expensive, but not as much as the first option)

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

Socket.io with React behaves weirdly after 5 messages

I am trying to build a basic chat application using socket.io and react but have ran into a weird problem. The app works like it is expected to for the first 5 messages and then after that the 6th message takes too long to load and often some of the previous messages don't show up in the chat box. Would be glad if someone could help.
Here is the code I have for backend:
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
io.on('connection', socket => {
socket.on('message', ({ name, message }) => {
io.emit('message', { name, message });
console.log(message);
console.log(name);
});
});
http.listen(4000, function () {
console.log('listening on port 4000');
});
Here is the code I have in my App.js:
import React, { useState, useEffect } from 'react';
import io from 'socket.io-client';
const App = props => {
const socket = io.connect('http://localhost:4000');
const [details, setDetails] = useState({ name: '', message: '' });
const [chat, setChat] = useState([]);
useEffect(() => {
socket.on('message', ({ name, message }) => {
setChat([...chat, { name, message }]); //same as {name:name,message:message}
});
});
const onMessageSubmit = e => {
e.preventDefault();
const { name, message } = details;
socket.emit('message', { name, message });
setDetails({ name, message: '' });
};
return (
<div>
<form onSubmit={onMessageSubmit}>
<input
type='text'
value={details.name}
onChange={e => setDetails({ ...details, name: e.target.value })}
/>
<input
type='text'
value={details.message}
onChange={e => setDetails({ ...details, message: e.target.value })}
/>
<button>Send</button>
</form>
<ul>
{chat &&
chat.map((chat, index) => (
<li key={index}>
{chat.name}:{chat.message}
</li>
))}
</ul>
</div>
);
};
export default App;
That's because every time, you update the state, useEffect callback runs, basically you subscribe to message again and again.
And after few iterations, you've multiple subscriptions trying to update the same state. And because of the setState's asynchronous nature, you're seeing the weird behavior.
You need to subscribe only once, you can do that by passing empty dependency argument to useEffect which will make it work like componentDidMount
useEffect(() => {
socket.on('message', ({ name, message }) => {
setChat([...chat, { name, message }]);
});
}, []);
Edit - To handle the asynchronity and take in account the previous chat, you need to setState via callback
useEffect(() => {
socket.on("message", ({ name, message }) => {
setChat((prevChat) => prevChat.concat([{ name, message }]));
});
}, []);
You might want to cleanup when your component un-mounts. Please have a look at the official docs.

Resources