Websocket is undefined immediately after receiving message in react

Viewed 993

I'm using websocket in react. This is the code for the component:

import React, { useState, useEffect } from "react";

export default function Component(props) {
  const [socket, setSocket] = useState();

  const parseMessage = (msg) => {
    if (msg[0] !== "R") sendMessage("123"); // ignore the very first message from the socket. 
  };

  const sendMessage = (msg) => socket.send(msg); // error at this line

  useEffect(() => {
    const socket = new WebSocket("wss://ws.ifelse.io/");
    socket.addEventListener("message", ({ data }) => {
      if (socket) parseMessage(data);
    });
    setSocket(socket);
  }, []);

  const sendMsg = () => {
    socket.send("test");
  };

  return <button onClick={() => sendMsg("clicked")}>send msg</button>;
}

I'm getting this error: TypeError: Cannot read properties of undefined (reading 'send') at the marked line. The WebSocket is just an echo server, it sends back the same thing you send it.

If I wrap the socket.send in a try-catch block, I can still send and receive messages from the WebSocket using the button, but the error still occurs at that line in sendMessage.

It's clear that the socket variable is not undefined as I'm able to send and receive messages before and after the error occurs.

So my question is why is the socket variable undefined only for the brief period after receiving a message, and what is the fix for this issue.

4 Answers

Better solution would be to initialize the socket outside

import React, { useState, useEffect } from "react";

const socket = new WebSocket("wss://ws.ifelse.io/")

export default function Component(props) {
  const ws = useRef(socket)
  
  useEffect(() => {
    ws.current?.addEventListener("message", ({ data }) => {
      parseMessage(data);
    });
    return () => {
      ws.current?.removeAllListeners()
    }
  }, [])

  const parseMessage = (msg) => {
    if (msg[0] !== "R") sendMessage("123"); // ignore the very first message from the socket. 
  };

  const sendMessage = (msg) => ws.current?.send(msg);


  const sendMsg = () => {
    ws.current?.send("test");
  };

  return <button onClick={() => sendMsg("clicked")}>send msg</button>;
}

The useEffect runs just once and in that moment the socket is still undefined. Function sendMessage references undefined socket when the effect runs. When the socket is set using setSocket, component will rerender, but the new instance of sendMessage (now referencing the existing socket) will never be used, because the effect will not run again.

It is better to use ref in this case.

export default function Component(props) {
  const socket = useRef();

  const sendMessage = (msg) => socket.current.send(msg); 

  useEffect(() => {
    socket.current = new WebSocket("wss://ws.ifelse.io/");
    socket.current.addEventListener("message", ({ data }) => {
      parseMessage(data);
    });
    return () => {
       ... release resources here
    }
  }, []);
  
  ...
}

When you use useState you create a state variable with an update state function. You also provide the initial value of the state variable in useState(initialState).

In your case you are not passing anything (i.e. you are telling React it's undefined, that's why at this line:

  const sendMessage = (msg) => socket.send(msg); // error at this line

your code cannot use socket.send because socket is undefined.

Your useEffect runs after the return function and then the socket instance is created as well as set to socket state variable.

You need to make sure that you are only sending message over socket after socket is created.

For more on useEffect you can read this post: https://jayendra.xyz/2019/06/17/how-useeffect-works/

You need to wait for the socket to be established first and then set the socket using setSocket in the open event. The way you are doing it now expects the first message to be sent by the server and then you setSocket, if the server does not send a message before you click on the button, the socket instance is not set in your state. Do this instead:

socket.addEventListener('open', function (event) {
    setSocket(socket);
});

Here is the full code

import React, { useState, useEffect } from "react";

export default function Component(props) {
  const [socket, setSocket] = useState();
  const [disabled, setButtonDisabled] = useState(true);

  const parseMessage = (msg) => {
    if (msg[0] !== "R") sendMessage("123"); // ignore the very first message from the socket. 
  };

  const sendMessage = (msg) => socket.send(msg); // error at this line

  useEffect(() => {
    const socket = new WebSocket("wss://ws.ifelse.io/");

    socket.addEventListener('open', function (event) {
     setSocket(socket);
     setButtonDisabled(false); 
    }); 

    socket.addEventListener("message", ({ data }) => {
      if (data) parseMessage(data);
    });

    return ()=>{
      socket.close();
    };
  }, []);

  const sendMsg = () => {
    if(socket) socket.send("test");
  };

  return <button disabled={disabled} onClick={() => sendMsg("clicked")}>send msg</button>;
}

Related