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.