Trying to get a basic chat app going and having problems with excessive rerenders when a message is sent. Here is the applicable code for the client:
const [chatMessages, setChatMessages] = useState([]);
const sendChat = (e) => {
socket.emit("sendMessage", e.target.elements.message.value);
}
useEffect(() => {
socket.on("receiveMessage", (chatMessage) => {
setChatMessages([...chatMessages, chatMessage]);
console.log(chatMessages);
});
}, [chatMessages]);
return (
{chatMessages.map((message) => <p>{message}</p>)}
)
Then, on the server:
io.on("connection", (socket) => {
socket.on("sendMessage", (chatMessage) => {
console.log("message sent");
io.to(roomId).emit("receiveMessage", chatMessage);
});
}
When I do this, the message is successfully sent and received but it results in it happening lots of times (console):
[]
[]
[{...}]
[{...}]
(2) [{...}, {...}]
On the third message this is what gets logged. By the the sixth or seventh message the whole page comes to a grinding halt as it logs about 100 times.
I have tried the following:
Having an empty dependency array in the
useEffect()
. This does fix the rerenders, but introduces a new problem. The latest message is the only one that is saved and replaced the last one, so you can only see one message at a time.Taking it out of
useEffect()
all together. This just worsens the problem and causes even more rerenders per message.
Any help would be appreciated. Thank you!
CodePudding user response:
Issue
You are creating socket event handlers when the chatMessages
state updates but not cleaning them up. If you edit your code or the component rerenders, etc.... then yet another socket event handler is added. The multiple handlers will start to stack up and enqueue multiple unexpected state updates.
Additionally, since React state updates are asynchronously processed you can't log the state immediately after enqueueing the update and expect to see the updated state. Use a separate useEffect
hook for this.
Solution
Add an
useEffect
cleanup function to remove the event handler and re-enclose the updatedchatMessages
state array in the handler callback.useEffect(() => { const handler = (chatMessage) => { setChatMessages([...chatMessages, chatMessage]); } socket.on("receiveMessage", handler); return () => socket.off("receiveMessage", handler); }, [chatMessages]);
Add an
useEffect
cleanup function, remove the dependencies so the effect runs once on component mount, and use a functional state update to correctly update from the previous state instead of the initial state in the callback enclosure.useEffect(() => { const handler = (chatMessage) => { setChatMessages(chatMessages => [...chatMessages, chatMessage]); } socket.on("receiveMessage", handler); return () => socket.off("receiveMessage", handler); }, []);
Between the two the second option is the more optimal solution, but which you choose is your decision.
To log the chatMessages
state updates:
useEffect(() => {
console.log(chatMessages);
}, [chatMessages]);
CodePudding user response:
Since you have a dependency on chatMessages
, every time the chatMessages
changes, it creates a new listener. And that's why it becomes slower and slower as and when more messages come in.
You could do two things:
- You maintain the
chatMessages
locally within theuseEffect
method. And you can spread that array and just callsetChatMessages
with the spread array. When you do this, you can remove thechatMessages
dependency touseEffect
and still have all messages. As a good practice, you should return a function that will remove the event listener when the component unmounts.
const [chatMessages, setChatMessages] = useState([]);
const sendChat = (e) => {
socket.emit("sendMessage", e.target.elements.message.value);
}
useEffect(() => {
let localMessages = [];
const callback = (chatMessage) => {
localMessages = [...localMessages, chatMessage];
setChatMessages(localMessages);
console.log(localMessages);
};
socket.on("receiveMessage", callback);
return () => {
socket.off("receiveMessage", callback);
}
}, []);
return (
{chatMessages.map((message) => <p>{message}</p>)}
)
- You can probably use
useRef
for storing the values. However that will not trigger the re-render of the UI when the value changes and probably that's not what you want. So this may not be a good choice.