Why does the useEffect() seem to reset my local state after receiving data from a socket?

Viewed 1083

I have a simple react component that sends and receives messages via SocketIO. My state hooks look like this

 const [newMessage, createMessage] = useState('');
 const [messages, setMessage] = useState([]);
 const [isConnected, setConnection] = useState(false);

The idea is that the user can compose and send the message through the socket and then will receive the message back. The user inputs some text into the text area. The contents of the message are saved to the "newMessage" local state variable - implementation details below if necessary - and then the user sends the newMessage through the socket.

const sendMessage = () => {
    const currentMessage = {...newMessage}; //reduced for the sake of brevity

    socket.emit('message', currentMessage);
    createMessage('');
  };


const composeMessage = (
    <>
      <div>
        <textarea rows="2" cols="28" placeholder="Chat Message" onKeyDown={(e) => handleKeyDown(e)} onChange={(e) => createMessage(e.currentTarget.value)} value={newMessage} />
      </div>
      <div>
        <button onClick={sendMessage}><i className="ra ra-horn-call" /></button>
      </div>
    </>
  );

Serverside, the message is received and immediately transmitted back to the room. I put the function that listens for the message response into the useEffect hook. The idea was to use the hook in place of componentDidMount, so I originally implemented it by passing an empty array as the second argument to useEffect. When I did this, any time a message was received, it would appear to clear out my "messages" state and then replace the contents with the new message. From the browser, it looked like there was only ever one message that could be held in state at a time - the previous message kept getting replaced. I tired passing the "messages" state in the second argument array and the message now appeared to get appended to the state and the component re-rendered as it was received -cool right, thats what I wanted?

I noticed that I was having performance issues on every subsequent message and ultimately discovered that the socket listener was getting re-applied each render. In order to stop this a colleague suggested that I add a boolean to state that should prevent the socket from getting re-added whenever there is a re-render. I updated the component and the result was the same as it was the first time when I was not passing the messages state into the second argument - it kept replacing the first message in state and did not append new messages. I just want the listener to update the local state and rerender the messages any time a new message comes through the socket. I am kind of at a loss for how to do this with hooks. Anyone have any ideas?

The final iteration of the useEffect hook that I wrote is below.

  useEffect(() => {
    debugger
    if (!isConnected) {
      socket.on('message', (message) => {
        const nextState = messages.slice();
        nextState.push(message);
        setMessage(nextState);
        debugger
      });
      setConnection(true);
    }
  }, [messages, setConnection]);
1 Answers

From your code, i think useEffect will run and make multiple subscriptions to listen on every message, on each render, whenever there's a change to message and connectionState. This is because a react component will render whenever there's a state change. As websocket connections and listeners can be considered side effects, you have to implement a return callback to handle them when your component unmounts.

Assuming all your websocket handling and chat message logic are within a single react component, you probably only need to create the web socket connection and listener once on mount like so, and provide a return call back to handle the unmounts, for scenarios like when you do a page refresh, or your component relies on a parent state which might trigger a re-render:

 useEffect(() => {
            socket.connect();
            socket.on('connect', () => {
                console.log('socket connected:', socket.connected);
            });
            socket.on('message',...);
        }
        return () => {
            console.log('websocket unmounting!!!!!');
            socket.off();
            socket.disconnect();

        };
    }, []);

Related