diff --git a/frontend/src/ws/useChatSocket.ts b/frontend/src/ws/useChatSocket.ts index 8afa0d7..4a960d0 100644 --- a/frontend/src/ws/useChatSocket.ts +++ b/frontend/src/ws/useChatSocket.ts @@ -29,16 +29,30 @@ export function useChatSocket({ roomId, onMessage, onUnauthenticated }: UseChatS socketRef.current = ws ws.onopen = () => { + // Guards against React StrictMode's dev-only double-invoke of this + // effect (mount -> cleanup -> mount again): the first socket gets + // abandoned in cleanup, but its own open/close events can still + // fire asynchronously afterward. Without this check, a stale + // socket's callbacks can stomp on state that the second (real) + // socket already owns. + if (socketRef.current !== ws) return reconnectDelay = RECONNECT_BASE_DELAY_MS setConnected(true) ws.send(JSON.stringify({ type: 'join', room_id: roomId })) } ws.onmessage = (event) => { + if (socketRef.current !== ws) return onMessageRef.current(JSON.parse(event.data) as ServerEnvelope) } ws.onclose = (event) => { + // Same guard as onopen -- a stale/abandoned socket's close event + // must not null out the reference to whatever socket has actually + // taken over since (this was a real bug: the abandoned socket's + // delayed onclose was silently orphaning a perfectly live + // connection, with nothing left referencing it to send on). + if (socketRef.current !== ws) return setConnected(false) socketRef.current = null