node.jsreactjsexpresssockets

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;


Solution

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