'How to write a generic Stomp Client in React using React Hooks?

Problem

I try to write a generic Stomp Client (using Websockets) in React. To realize it I want to use React's Hook API. As I am new to React and not really experienced in JavaScript, I have not a very good intuition on the style of my code. The target component is supposed to

  • use a Stomp Client to connect to a server with a single web socket connection and leave it open (until e.g. the user closes the browser tab or the application crashes :-D )
  • try to reconnect if the connection closes (which should be ensured by the Stomp Client import {Client} from "@stomp/stompjs";)
  • provide an API for child components to subscribe to a topic and publish a message (for now)
  • be a generic wrapper class that can be reused around different components

Currently the component fulfills all of the above mentioned criteria. However, there are some drawbacks. The most important one is that the ESLinter complains about how I use the useEffect-hook complaining about ESLint: React Hook useEffect has a missing dependency: 'subscriptions'. Either include it or remove the dependency array.(react-hooks/exhaustive-deps).

As I want to have the Stomp Client only being set up once, I give it an empty dependency array in useEffects. However, I loop over all values in subscriptions (which is managed by a useState-hook) to ensure that after a disconnect the app connects to all subscriptions that the app was subscribed to, before the disconnect happened. (The list of subscriptions can change over time). I do not want to add the subscriptions array to the dependency array of useEffect since this will trigger a reconnect of the Stomp Client and handleSubscription not only adds a subscription to the subscriptions array but also subscribes via the Stomp Client.

So I am wondering how I can change below code to remove the linter warning (and in general how to improve the code). Is it a good idea to put the Stomp CLient in useState-hook and then initialize it in useEffect?

What I have tried

My Code

import React, {useEffect, useState} from "react";
import {Client} from "@stomp/stompjs";
import Loading from "./Loading";

const StompProvider = (props) => {

    const [subscriptions, setSubscriptions] = useState([]);
    const [stompClient, setStompClient] = useState(null);

    useEffect(() => {
            console.log('Initialize stomp client...')
            const stompConfig = {
                connectHeaders: {},
                brokerURL: "ws://localhost:8080/gammick/websocket",
                debug: function (str) {
                    console.log('STOMP: ' + str);
                },
                // If disconnected, it will retry after 200ms
                reconnectDelay: 200,
                // Subscriptions should be done inside onConnect as those need to reinstated when the broker reconnects
                onConnect: (frame) => {
                    for (const topic of subscriptions) {
                        const topicName = topic.name;
                        console.log("Subscribing to topic ", topicName);
                        // The return object has a method called `unsubscribe`
                        stompClient.subscribe(topicName, topic.onMessage);
                    }
                    setStompClient(stompClient);
                }
            };
            const stompClient = new Client(stompConfig);
            stompClient.activate();

            // TODO: Implement cleanup function on stomp client, e.g .unsubscribing from subscriptions or closing client
            return () => {
            };
        }, []
    );

    function handleSubscription(subscription) {
        console.log('Subscribe to topic: ', subscription);
        stompClient.subscribe(subscription.topic, subscription.onMessage);
        setSubscriptions([...subscriptions]);
    }

    function handlePublishMessage(destination, message) {
        console.log('Publishing message: ', message, 'to destination:', destination);
        stompClient.publish({
                destination: destination,
                body: message
            }
        )
    }

    if (!stompClient) {
        return <Loading/>;
    }
    const childrenWithProps = React.Children.map(props.children, child => {
        if (React.isValidElement(child)) {
            return React.cloneElement(child, {
                onSubscribe: (subscription) => handleSubscription(subscription),
                onPublishMessage: (destination, message) => handlePublishMessage(destination, message)
            });
        }
        return child;
    });

    return <div>{childrenWithProps}</div>;
    // return (
    //     <Lobby
    //         onSubscribe={(subscription) => handleSubscription(subscription)}
    //         onPublishMessage={(destination, message) => handlePublishMessage(destination, message)}
    //     />
    // );

}

export default StompProvider;


Solution 1:[1]

Well, I came here looking for the same solution but I ended up using react-stomp-hooks library.

https://www.npmjs.com/package/react-stomp-hooks

This library supports typescript and has hooks that you can use within a react component whenever you need to need to subscribe to channel e.g

 useSubscription(
        `/path/${channelId}`,
        (message) => onMessageReceived(message),
        {header: headerValue});

Besides you can create a single connection using <StompSessionProvider> in the top component i.e react app.js component or index.js then subscribe to a channel anywhere in your app using useSubscription hook.

More to that <StompSessionProvider> accepts all / most of stomp-config properties e.g reconnectDelay, connectionTimeout, onConnect, connectHeaders e.t.c

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 p.maimba