Search code examples
javascriptreactjsreact-hookswebsocketmqtt.js

How to connect to Websocket server with a config that comes from an api?


I'm trying to connect to a websocket server in react but I have to get the connection config from an api. I can't find the right pattern to do that.

I need to:

  1. Get the config from the api using react-query
  2. Create a client when the config is available
  3. Save the client in a state
  4. Update the client when the config changes
  5. Disconnect from the client on unmount

I'm using MQTT.js here because I want to connect to a mqtt broker, but it's the same thing as websockets. I just want to find the right pattern

Here is some of my attempts:

import mqtt from "mqtt"

export function useMqttClient() {
  const [client, setClient] = useState();

  const { data: config } = useQuery(/* ... */);
  
  useEffect(() => {
    if(config && !client) {
      setClient(mqtt.connect(config))
    }
  }, [config, client])
  
  // ...
}

The problem with this approach is that it doesn't end the connection (client.end()) when the component that uses useMqttClient unmounts. As the result, it tries to reconnect with the same config when the user navigates back to the same page and that's a problem for us. Also it doesn't update the client when config changes.

import mqtt from "mqtt"

export function useMqttClient() {
  const [client, setClient] = useState();

  const { data: config } = useQuery(/* ... */);
  
  useEffect(() => {
    if(config && !client?.connected) {
      setClient(mqtt.connect(config))
    }
    return () => client?.end()
  }, [config, client])
  
  // ...
}

This time I end the connection in the cleanup function but I end up in an infinite loop. client is an instance of a class and config is an object so both of them could cause the infinite loop. But I don't know how to stop it.


Solution

  • Your code example looks like the right pattern to me. It's just got the infinite loop you mentioned. If we resolve the infinite loop you ought to have something close to what you need:

    import mqtt from "mqtt"
    
    export function useMqttClient() {
      const [client, setClient] = useState();
    
      const { data: config } = useQuery(/* ... */);
      
      useEffect(() => {
        if(!config) return;
        const newClient = mqtt.connect(config);
        setClient(newClient);
        return () => newClient.end();
      }, [config]);
      
      // ...
    }
    

    I've eliminated the need for client to be a dependency of the effect hook. As a rule of thumb a useState's dispatch should never be called in a useEffect callback whose deps include the state which is why usually your linter automatically points it out as a potential infinite loop.