Socket.io with React behaves weirdly after 5 messages - node.js

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.

Related

React js socket io emit

im building a chat app with mern and socket.io, i tried to emit with callback but it emit function not value and when i delete setList(l=>[...l,data]) it emit (first problem).
And the second problem, when i recieve the message and push it to list twice (useEffect).
Client side:
import React,{ useState,useEffect } from 'react';
import socketIOClient from "socket.io-client";
function Room(props) {
const [message,setMessage]=useState('');
const [list, setList] = useState([]);
const ENDPOINT = "http://localhost:5000";
const socket = socketIOClient(ENDPOINT, {
credentials: true
});
const sendMessage=(e)=>{
e.preventDefault();
let data=message;
//the first problem is here
socket.emit('send',(data)=>{
setList(l=>[...l,message])
});
}
useEffect(()=>{
//the second problem is here
socket.on('rec',(data)=>{
setList(l=>[...l,data])
})
},[])
return (
<div>
<div>
{list!==[] && list!==null && list.map((el,i)=>{
return <p key={i}>{el}</p>
})}
</div>
<div>
<form>
<input value={message} onChange={e=>setMessage(e.target.value)} />
<button onClick={sendMessage}>Send</button>
</form>
</div>
</div>
)
}
export default Room;
Server Side:
const io = new Server(server, { cors: { origin: '*'}});
app.use((req,res,next)=>{
res.header("Access-Control-Allow-Credentials", true);
next();
})
dotenv.config();
io.on("connection", (socket) => {
socket.emit('greeting', {
greeting: 'Hello Client'
})
socket.on("send", (data) => {
io.emit("rec", data);
console.log(data)
});
});
server.listen(process.env.PORT, err => err?console.log('server error'):console.log(`server is running on PORT ${process.env.PORT}`));
//the first problem is here
socket.emit('send',(data)=>{
setList(l=>[...l,message])
});
has to be
socket.emit('send', message, (data)=>{
setList(l=>[...l,message])
});
(you missed the argument, see https://socket.io/docs/v4/client-api/#socketemiteventname-args)
2)
useEffect(()=>{
//the second problem is here
socket.on('rec',(data)=>{
setList(l=>[...l,data])
})
},[])
it's not really a problem, because you add the same message here and in sendMessage, by using setList twice. I suppose for the chat app you need to change io.emit("rec", data); with a different response.

TypeError: msg.map is not a function

I'm trying to create a realtime online chat using React, node and mongodb. In my opinion, the chat shoudl work this way: client send the object of the message he created to the server to save it via rest(works properly) and via socket so the socketserver "broadcast" the message to every socket that is in the same room(socket on connection is put in a room according to something store in localstorage). So, other client in the same room should then recive the message, and happend it to the chat. But it is not workink. In fact, the following error shows up: Uncaught TypeError: msg.map is not a function
This is my react code:
import {useState, useEffect} from 'react';
import axios from 'axios';
import { io } from "socket.io-client";
const Chat = () => {
const [msg, setMsg] = useState([]);
const [message, setMessage] = useState('');
const socket = io("http://localhost:5050");
useEffect(() => {
if(v.group.name){
axios.get(`http://localhost:5050/chat/getmsg/${v.group.name}`)
.then(group => {
setMsg(group.data)
})
}
}, [v.group.name])
useEffect(() => {
if(localStorage.getItem('isG') === '1'){
socket.on("connect", () => {
socket.emit("groupName", {id:localStorage.getItem('gruop')})
})
socket.on("message", messageS => {
if(messageS.sender !== localStorage.getItem('user'))
setMsg(...msg, messageS)
})
}
// eslint-disable-next-line
}, [socket])
const sendMSG = (e) => {
e.preventDefault();
if(message !== ""){
axios.post("http://localhost:5050/chat/sendmsg", {name:v.group.name, sender:localStorage.getItem('user'), text:message})
.then(() => {
setMessage('');
socket.emit("message", {name:v.group.name, sender:localStorage.getItem('user'), text:message})
setMsg(...msg, {name:v.group.name, sender:localStorage.getItem('user'), text:message})
});
}
}
return <div className="containerLogin1">
<div>
<h3>Chat Name</h3>
</div>
<div className="chatSpace">
{
msg.map((m) => {
return <p key={m._id}>{m.text}</p>
})
}
</div>
<form className="sMSG">
<input type="input" style={{'border':'2px solid black'}} value={message} onChange={(e) => setMessage(e.target.value)}/>
<button className="buttSend" onClick={sendMSG} spellCheck="false">Send</button>
</form>
</div>
}
And this is server code, but I think that he is working fine:
....
const httpServer = app.listen(port, () => {console.log(`Server listening on port ${port}`)});
const { Server } = require("socket.io");
const io = new Server(httpServer, {
cors : {
origin: "*",
methods:["GET", "POST"]
}
} );
io.on("connection", (socket) => {
let currentRoom = "";
socket.on("groupName", msg => {
socket.join(msg.id + "")
currentRoom = msg.id
})
socket.on("text-change", newText => {
io.to(currentRoom).emit( "text-change", {text:newText, emitter:socket.id})
})
socket.on("message", message => {
io.to(currentRoom).emit("message", message);
})
})
I tryied with a bounch of console.log to see where the error could be, but I couldn't find out. It seems that somewhere in the code, msg turns from an array to an object, and so map function crashes. Can someone please help me? Thanks
You have these 2 lines in your code where you are trying to copy the last array and add new object to it:
setMsg(...msg, messageS);
And:
setMsg(...msg, {name:v.group.name, sender:localStorage.getItem('user'), text:message});
These parts are the problem, you should add [] around them. So:
setMsg([...msg, messageS]);
setMsg([...msg, {name:v.group.name, sender:localStorage.getItem('user'), text:message}]);

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)

socket io server connected = true, but client connected = false

iam new to the websocket, and i'm currently wanting to create a chat app using socket.io
i follow the documentation on the socket.io website and successfully connect the server to the client
but as i want to emit from the server to the client or vise versa i found out that my client is not connected to the server as the property disconnected = true
and my guess is that because of this i can't use the emit event
can someone explain whats going on?
note: both using version 2.2.0 of socket.io
here is the screenshot of whats happening
server
and the code of the server
const app = require('express')()
const http = require('http').createServer(app)
const io = require('socket.io')(http)
io.on('connection', socket => {
console.log(socket.connected)
socket.on('message', ({ name, message }) => {
console.log(name)
console.log(message)
io.emit('message', { name, message })
})
})
http.listen(4000, function () {
console.log('listening on port 4000')
})
and here is the screenshot of the client
client
and here is the code of the client
import React, { useEffect, useRef, useState } from "react"
import io from "socket.io-client"
import "./App.css"
function App() {
const [state, setState] = useState({ message: "", name: "" })
const [chat, setChat] = useState([])
const socketRef = useRef()
useEffect(
() => {
socketRef.current = io("http://127.0.0.1:4000")
socketRef.current.connect()
console.log(socketRef.current)
socketRef.current.on('connect', function () {
console.log("masuk")
});
socketRef.current.on("message", ({ name, message }) => {
setChat([...chat, { name, message }])
})
return () => socketRef.current.disconnect()
},
[chat]
)
const onTextChange = (e) => {
setState({ ...state, [e.target.name]: e.target.value })
}
const onMessageSubmit = (e) => {
const { name, message } = state
console.log(name)
console.log(message)
socketRef.current.emit("message", { name, message })
e.preventDefault()
setState({ message: "", name })
}
const renderChat = () => {
return chat.map(({ name, message }, index) => (
<div key={index}>
<h3>
{name}: <span>{message}</span>
</h3>
</div>
))
}
return (
<div className="card">
<form onSubmit={onMessageSubmit}>
<h1>Messenger</h1>
<div className="name-field">
<TextField name="name" onChange={(e) => onTextChange(e)} value={state.name} label="Name" />
</div>
<div>
<TextField
name="message"
onChange={(e) => onTextChange(e)}
value={state.message}
id="outlined-multiline-static"
variant="outlined"
label="Message"
/>
</div>
<button>Send Message</button>
</form>
<div className="render-chat">
<h1>Chat Log</h1>
{renderChat()}
</div>
</div>
)
}
export default App
i got it already, its because the version is not compatible between the server and the client

React page not updating on socket message push despite the data being sent

My socket.io chat app is basically fully functional but the react page is not updating when the message event is emitted to the group id. Currently I am using a map function to run through all of the messages in an array... Each time a message is sent it is saved in a database for future use, but that doesn't seem to be the problem because I removed that part and the react component still didn't update. Any help is greatly appreciated... code below --
// Import React dependencies.
import React, { Component } from "react";
import io from 'socket.io-client'
import axios from 'axios'
import Message from '../Message/Message'
import uuid from 'react-uuid'
// Import the Slate components and React plugin.
const ENDPOINT = 'http://localhost:5000/'
export const socket = io.connect(ENDPOINT)
export class LiveChat extends Component {
constructor() {
super()
this.state = {
messages: [],
message: "",
id: "",
username: ""
}
}
changeHandler = (e) => {
this.setState({
message: e.target.value
})
socket.emit('typing',)
}
clickHandler = () => {
const data = {
messageId: uuid(),
username: this.state.username,
groupId: this.state.id,
message: this.state.message,
}
console.log(data.messageId)
socket.emit('message', data)
}
async componentDidMount() {
const { id } = this.props.match.params
console.log(this.state)
const response = await axios.get('http://localhost:5000/api/users/userInfo', { withCredentials: true })
const messages = await axios.get(`http://localhost:5000/live/${id}`)
console.log(messages.data)
this.setState({
messages: messages.data,
username: response.data.user,
id: id
})
console.log(this.state)
socket.on('typing', data => {
console.log(data)
});
socket.on(`message-${this.state.id}`, data => {
console.log(data)
this.state.messages.push(data)
})
}
render() {
return (
<div>
<input placeholder="message" type="text" onChange={e => this.changeHandler(e)} />
<button onClick={this.clickHandler}>Submit</button>
{this.state.messages.map((message) => (
<Message key={message.messageId} message={message} />
))}
</div>
)
}
}
export default LiveChat;
relevant API code:
io.on("connection", (socket) => {
console.log("New client connected");
socket.on('new-operations', function (data) {
groupData[data.groupId] = data.newValue;
console.log(groupData[data.groupId])
io.emit(`new-remote-operations-${data.groupId}`, data)
})
socket.on('typing-message', function (message) {
io.emit('typing', "user is typing")
})
socket.on('message', function (data) {
console.log(data)
Chat.create({
messageId: data.messageId,
username: data.username,
groupId: data.groupId,
message: data.message
})
io.emit(`message-${data.groupId}`, data)
})
io.emit("init-value", initValue)
socket.on("disconnect", () => {
console.log("Client disconnected");
clearInterval(interval);
});
});
The problem is caused by the fact that setState is an asynchronous function. Because setState doesn't update the state immediately, in your codes, after you setState for id, you read it in the socket.on function, in this case, it is not guaranteed to get the updated state.
socket.on(`message-${this.state.id}`, data => {...
For more info, refer to this link below.
setState doesn't update the state immediately

Resources