Home > Mobile >  How to use context values in useEffect, that only runs once
How to use context values in useEffect, that only runs once

Time:11-15

i've got an interesting problem here. I am building a react application using web socket communication with the server. I create this websocket in a useEffect hook, which therefore cannot run multiple times, otherwise i'd end up with multiple connections. In this useEffect, however i intend to use some variables,which are actually in a context (useContext) hook. And when the context values change, the values in useEffect , understandably, don't update. I've tried useRef, but didn't work. Do you have any ideas?

const ws = useRef<WebSocket>();

  useEffect(() => {
    ws.current = new WebSocket("ws://localhost:5000");
    ws.current.addEventListener("open", () => {
      console.log("opened connection");
    });

    ws.current.addEventListener("message", (message) => {
      const messageData: ResponseData = JSON.parse(message.data);
      const { response, reload } = messageData;

      if (typeof response === "string") {
        const event = new CustomEvent<ResponseData>(response, {
          detail: messageData,
        });
        ws.current?.dispatchEvent(event);
      } else {
        if (reload !== undefined) {
          console.log("general info should reload now");
          GeneralInfoContext.reload(reload);
        }
        console.log(messageData);
      }
    });
  });

The web socket is stored as a ref for better use in different functions outside of this useEffect block

Note: the context value to be used is actually a function, GeneralInfoContext.reload()

CodePudding user response:

Solution with split useEffect

You can split the logic that opens the websocket connection vs. the one that adds the message handler into separate useEffects - the first can run once, while the second can re-attach the event every time a dependency changes:

useEffect(() => {
    ws.current = new WebSocket("ws://localhost:5000");
    ws.current.addEventListener("open", () => {
        console.log("opened connection");
    });
}, []);

useEffect(() => {
    const socket = ws.current;
    if(!socket) throw new Error("Expected to have a websocket instance");
    const handler = (message) => {
        /*...*/
    }
    socket.addEventListener("message", handler);
    // cleanup
    return () => socket.removeEventListener("message", handler);
}, [/* deps here*/])

The effects will run in order so the second effect will run after the first effect has already set ws.current.


Solution with callback ref

Alternatively you could put the handler into a ref and update it as necessary, and reference the ref when calling the event:

const handlerRef = useRef(() => {})

useEffect(() => {
    handlerRef.current = (message) => {
        /*...*/
    }
    // No deps here, can update the function on every render
});

useEffect(() => {
    ws.current = new WebSocket("ws://localhost:5000");
    ws.current.addEventListener("open", () => {
        console.log("opened connection");
    });

    const handlerFunc = (message) => handlerRef.current(message);
    ws.current.addEventListener("message", handlerFunc);
    return () => ws.current.removeEventListener("message", handlerFunc);
}, []);

It's important that you don't do addEventListener("message", handlerRef.current) as that will only attach the original version of the function - the extra (message) => handlerRef.current(message) wrapper is necessary so that every message gets passed to the latest version of the handler func.

This approach still requires two useEffect as it's best to not put handlerRef.current = /* func */ directly in the render logic, as rendering shouldn't have side-effects.


Which to use?

I like the first one personally, detaching and reattaching event handlers should be harmless (and basically 'free') and feels less complicated than adding an additional ref.

But the second one avoids the need for an explicit dependency list, which is nice too, especially if you aren't using the eslint rule to ensure exhaustive deps. (Though you definitely should be)

CodePudding user response:

You should pass an empty array as the second parameter to the useEffect, so it this case it becomes akin to the componentDidMount() logic of react

useEffect(() => { 
 ...your websocket code here
}, [])

CodePudding user response:

You can provide useEffect with a list of variables and useEffect will re-run when these variables change.

This is a little example:

const [exampleState, setExampleState] = useState<boolean>(false);

useEffect(() => {
  console.log("exampleState was updated.");
}, [exampleState]);
  

An example from reactjs website:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
  • Related