Home > Back-end >  Socket.io and React: Proper way too receive data from server and set state in a component?
Socket.io and React: Proper way too receive data from server and set state in a component?

Time:02-18

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:

  1. 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.

  2. 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

  1. Add an useEffect cleanup function to remove the event handler and re-enclose the updated chatMessages state array in the handler callback.

     useEffect(() => {
       const handler = (chatMessage) => {
         setChatMessages([...chatMessages, chatMessage]);
       }
    
       socket.on("receiveMessage", handler);
    
       return () => socket.off("receiveMessage", handler);
     }, [chatMessages]);
    
  2. 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:

  1. You maintain the chatMessages locally within the useEffect method. And you can spread that array and just call setChatMessages with the spread array. When you do this, you can remove the chatMessages dependency to useEffect 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>)}
)

  1. 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.
  • Related