I have the following component:
function SocketClient() {
const [socket, setSocket]= React.useState(null);
const [msg, setMsg] = React.useState([]);
function handleMessage(event) {
setMsg([...msg, String(event.data)]);
}
if(!socket){
const newSocket = new WebSocket("ws://localhost:8000/ws");
newSocket.addEventListener("open", function(){
newSocket.send("Hello Server!");
});
newSocket.addEventListener("message", handleMessage);
setSocket(newSocket);
}
function click() {
if (socket) {
socket?.send("hooooooonk");
}
}
return (
<div>
<p>Shared Component</p>
<button onClick={click}>HONK</button>
<pre><code>
{JSON.stringify(msg, null, " ")}
</code></pre>
</div>
);
}
I have been trying different ways of creating a websocket connection inside a react component. This is one of a few methods I have tried, in which I store the socket in local state, and then only create a socket if there currently isn’t one (if(!socket) {...), to avoid creating a new connection every time the component refreshes.
However, when I add the event listeners inside of the conditional statement, I ran into the following issue: the msg variable (also a local state variable) gets stuck in its initial value.
- So, when the component loads,
msgis an empty Array. - Once the websocket connection is created and the "open" handler runs, the value of
msgis set to["Hello Server!"], and this reflects on the component’s body. - However, after I send a new message, I would expect
msgto have a value of["Hello Server!", "hooooooonk"], but the last value just keeps replacing the existing one. - I verified that everytime
handleMessageruns it is reading the value ofmsgas an empty array (or whatever value I initialized it to).
So my question is, why does this happen? My understanding of closures is that handleMessage should keep reading the updated value of msg. Am I wrong in that understanding or is this something introduced by the hooks API?
>Solution :
It is always safest to use the functional form of the useEffect setter:
setMsg(msg => [...msg, String(event.data)]);
This will always get the current value of the variable as the argument and avoids a lot of common pitfall.
I believe in your case the pitfall is that you redefine the handleMessage function every time the component renders, and what msg refers to at that point is unclear — seems like at the time of rendering react still uses the default value of msg. There are other ways to avoid this pitfall (I think useCallback might work here, too), but the above is probably the best and most compact solution.
I would also put your if(!socket){ block into a useEffect instead.