nullptr
nullptr

Reputation: 4484

Websocket is undefined immediately after receiving message in react

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.

Upvotes: 2

Views: 2611

Answers (4)

Mike Grant
Mike Grant

Reputation: 209

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>;
}

Upvotes: -1

dkl
dkl

Reputation: 3942

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
    }
  }, []);
  
  ...
}

Upvotes: 1

Medet Tleukabiluly
Medet Tleukabiluly

Reputation: 11930

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>;
}

Upvotes: 4

Jayendra Sharan
Jayendra Sharan

Reputation: 1018

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/

Upvotes: -1

Related Questions