Reputation: 476
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
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
useEffect
callback within the callback itself, instead of outside of it;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
Reputation: 436
You can simply change setHistory([...history, {text: e.data}])
with the following code setHistory(history=> [...history, {text: e.data}])
Upvotes: 6