0

I'm trying to use websocket with hooks and starting it on useEffect. But the states is always the initial for the websocket.

I know that it happens because useEffect creates a closure, but I didn't find a workaround.

I simplified my code to make easily understandable. I need to make decisions according to activeContracts, but it doesn't change. I tried not to use ws as a state, but then it is lost on new renderings.

function Bitstamp() {
    const [activeContracts, setActiveContracts] = useState(["btcusd"]);
    const [ws, setWs] = useState();

    useEffect(
        () => {
            let newWs = new WebSocket("wss://ws.bitstamp.net");

            newWs.onopen = function () {
                activeContracts.map(contract => newWs.send({
                    "event": "bts:subscribe",
                    "data": {
                        "channel": "order_book_btcusd"
                    }
                }));
            };

            newWs.onmessage = function (evt) {
                console.log(activeContracts);
            };

            setWs(newWs);
        },
        [],
    );

    const handleContractChange = val => {
        setActiveContracts(val);
    };

    return (
        <ToggleButtonGroup vertical type="checkbox" value={activeContracts}
                           onChange={handleContractChange}>
            <ToggleButton
                value="btcusd">"btcusd"</ToggleButton>
            )}
            <ToggleButton
                value="btceur">"btceur"</ToggleButton>
            )}
        </ToggleButtonGroup>
    );
}

Is there anyway to the websocket notice changes on states? Should I initialize the websocket in another way or don't keep it as an state?

Update:

I added a useEffect tracking activeContracts and update onopen and onmessage and worked. Something like this:

useEffect(
    () => {
        setWs(ws => {
            ws.onopen = function () {
                activeContracts.map(contract => newWs.send({
                    "event": "bts:subscribe",
                    "data": {
                        "channel": "order_book_btcusd"
                    }
                }));
            };

            ws.onmessage = function (evt) {
                console.log(activeContracts);
            };

            return ws;
        })
    },
    [activeContracts],
);

1 Answers1

0

That's because useState runs only once when component initialises, due to the array argument being empty. You need to track activeContracts in that array, but because activeContracts is array itself you'll need to use some kind of deep compare function for it: https://stackoverflow.com/a/54096391/4468021

Clarity
  • 10,730
  • 2
  • 25
  • 35
  • why deep compare, isn't activeContracts immutable? – cubefox Jul 24 '19 at 12:53
  • because arrays are compared by reference, so the callback inside `useEffect` will run on every render. – Clarity Jul 24 '19 at 12:56
  • But track activeContracts would initialize the websocket again. Maybe I could create another useState tracking activeContracts and update only newWs.onmessage on ws and setWs again. – Carlos Ferro Jul 24 '19 at 13:16
  • In that case you can initialize websoket outside of use effect on only modify its `onopen` and `onmessage` inside `useEffect`. – Clarity Jul 24 '19 at 13:19
  • As I need start the ws after render, i think it's better initialize on useEffect and create another useEffect to update. Also, I dont't need to check if the values inside the array is the same, so a simple compare is ok. – Carlos Ferro Jul 24 '19 at 13:39