skip to Main Content

How can I set up a connection inside useEffect and at the same time decorate that connection with a custom hook? Custom hooks are not allowed to run inside useEffect and ref.current is not permitted during rendering. What is the proper way to wrap a connection with a React-friendly interface?

I believe the right way to set up a WebSocket connection in React, is with a useEffect hook, along the lines of

const connection = useRef(null);

useEffect(() => {
  const ws = new WebSocket(socketUrl);
  connection.current = ws

  return () => {
    ws.close();
    connection.current = null;
  }
}

In a similar way I envisage creating a RTCDataConnection, which follows the same connection interface with .send('...') and open, close, error and message events.

I am looking for a way to provide a React-like interface to these connections. I was thinking something like the following custom useConnection hook, which takes a JavaScript connection object and in return provides two React state variables lastMessage and readyState, and a method sendMessage:

export function useConnection(connection) {
  const [lastMessage, setLastMessage] = useState(null);
  const [readyState, setReadyState] = useState(null);

  const onReadyStateChange = useCallback(
    (e) => {
      console.info(e);
      setReadyState(connection.readyState);
    },
    [setReadyState]
  );

  const onMessage = useCallback(
    ({ data }) => {
      const message = JSON.parse(data);
      setLastMessage(message);
    },
    [setLastMessage]
  );

  function sendMessage(message) {
    const msgJSON = JSON.stringify(message);
    connection.send(msgJSON);
  }

  useEffect(() => {
    connection.addEventListener('open', onReadyStateChange);
    connection.addEventListener('close', onReadyStateChange);
    connection.addEventListener('error', onReadyStateChange);
    connection.addEventListener('message', onMessage);

    return () => {
      connection.removeEventListener('open', onReadyStateChange);
      connection.removeEventListener('close', onReadyStateChange);
      connection.removeEventListener('error', onReadyStateChange);
      connection.removeEventListener('message', onMessage);
    };
  }, [connection]);

  return { lastMessage, readyState, sendMessage };
}

I can’t find out, however, how to properly apply the hook to the connection.

  • By the rules of hooks, I can’t call useConnection inside useEffect. It throws "Invalid hook call" at me.
  • I can’t reference connection.current outside the useEffect either, because that violates the rules of useRef: It is not allowed to access the .current property during rendering.

So my question is, how do I bring the connection and the hook together? Or should I use a different approach altogether?

2

Answers


  1. What you would probably want is to call your hook at the top level of your component and then use it’s returned parts inside the useEffect()

    const {lastMessage, readyState, sendMessage} = useConnection(connection);
    
    useEffect(() => {
        sendMessage("Hi");
    }, [sendMessage]);
    

    Your approach looks fantastic!

    Login or Signup to reply.
  2. If you want to do this i.e. call the hook

    const {lastMessage, readyState, sendMessage} = useConnection(something);
    

    from inside useEffect because it is the only place where you have access to something so this is not possible.
    hooks should be called in the body of your function component not conditionally and not inside a loop you can think of them like an import.

    solution:

    create a state connection and use its value as useConnection function param

    const [connection, setConnection] = useState(null);
    const { lastMessage, readyState, sendMessage } = useConnection(connection);
    

    Note: you may need to make some work in your hook to handle null connection recived because this will happen when the hook is called when the component first mount

    Now from useEffect instead of call the hook, you just update connection so the component re-renders and your hook function will be recalled each time connection receives a new value. also note that the useEffect hook of your custom hook will fire each time received connection is updated, since it is included in its dependency array.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search