WebSocket in ReactJS is setting state with empty array

Thanks in advance for any suggestions here. I am implementing websocket in ReactJS here and my goal is populating my history state with upcoming 'onmessage' events. Oddly to me, setHistory is emptying my array after receiving messages from 'websocket.onmessage' events. It even removes my initial state. Simplified version below:

function NotificationWebSocket ({object_id}) {
const [history, setHistory] = useState([{text: "The first notification"}]) //

    function connectToWebSocket() {
        const wsStart = (window.location.protocol === "https:" ? "wss://" : "ws://")
        const url = 'localhost:8000/live/'
        let socket = new ReconnectingWebSocket(wsStart + url + object_id)
        socket.onmessage = e => {
            console.log(e.data) // logs the received data correctly
            setHistory([...history, {text: e.data}]) // ?? this is setting my history state array to null... ??
            setHistory(history.append({text: e.data})) //also cleans my history array here.
        }
    }

    useEffect(() => {
        connectToWebSocket()
    },[]) // so that we only connect to the websocket once

    return (
        <div>
            We are on a live connection to the server.
            {history.map(item=> <div>{item.text}</div>)}
        </div>
    )
}

Things to note here: I am using ReconnectingWebSocket library, although I tried not using it and the same thing occurs. My connectToWebSocket is being triggered from inside a useEffect cause otherwise I get a couple of websockets requests in the server.

Thanks again, Felipe.

Upvotes: 3

Views: 1783

Answers (2)

Gian Marco Toso
Gian Marco Toso

Reputation: 12136

You have a stale closure, this is a common mistake that is often made when using hooks with dependencies (such as useEffect, useMemo, useCallback). Take a look here.

The solution is to always include all the variables you're going to use within the useEffect callback in the dependency list. In your case it's hard to spot them, because most of them are hidden inside the connectToWebSocket function, which is updated at every render except... the one called by useEffect is the one that was created on the first render, and the onmessage callback closes over the state the component had at the first render.

Since you are using a WebSocket, it's not enough to just useEffect every time your history changes, because then you'll have multiple connections (one for every change), you also need to disconnect when the next effect is called, and you can do so by returning a function from the useEffect callback that disconnects from the WebSocket, so that the effect can connect again. This is also the example that is conceptually explained in React's docs, when showing an example about useEffect.

Something that is often suggested to avoid this kind of situation is to

  1. Define the function you're going to call within the useEffect callback within the callback itself, instead of outside of it;
  2. Use your favorite linter to warn you about missing dependencies in your useEffect (or similar) calls. If you try create-react-app, this is already configured for you.

Also, as @ako-javakhishvili suggests, whenever your next state depends on a previous value of the state itself, call the setter function with a callback instead of an immediate value. This will solve your problem in this case, but you will still have a stale closure and you should take care of that as well.

Upvotes: 1

You can simply change setHistory([...history, {text: e.data}]) with the following code setHistory(history=> [...history, {text: e.data}])

Upvotes: 6

Related Questions