create-react-app
create-react-app copied to clipboard
Duplicate Websocket connection only in dev mode
Describe the bug
WebSockets created conditionally and set on a useRef
connect twice. This only happens when the app is started in development mode with yarn start
but not when the app is served in production from the files generated by yarn build
.
Did you try recovering your dependencies?
Yes
Which terms did you search for in User Guide?
useRef, websocket
Environment
Environment Info:
current version of create-react-app: 4.0.1
running from /home/chetan/.npm/_npx/1695019/lib/node_modules/create-react-app
System:
OS: Linux 5.8 Ubuntu 20.04.1 LTS (Focal Fossa)
CPU: (12) x64 Intel(R) Core(TM) i7-10710U CPU @ 1.10GHz
Binaries:
Node: 12.20.1 - ~/.nvm/versions/node/v12.20.1/bin/node
Yarn: 1.22.10 - ~/.npm-packages/bin/yarn
npm: 6.14.11 - ~/.nvm/versions/node/v12.20.1/bin/npm
Browsers:
Chrome: Not Found
Firefox: 84.0.2
npmPackages:
react: Not Found
react-dom: Not Found
react-scripts: Not Found
npmGlobalPackages:
create-react-app: Not Found
Tested on Chromium 87.0.4280.141 and Firefox 84.0.2 on Ubuntu 20.04.
Steps to reproduce
yarn
yarn start
Expected behavior
One websocket connection is made to echo.websocket.org
.
Actual behavior
Two websocket connections are made to echo.websocket.org
, which are visible in the "Network" tab in browser dev tools.
Reproducible demo
https://github.com/chetbox/react-dev-websocket-bug
That is because React.StrictMode
renders the component twice (as it is intentional), that causes 2 connections get created.
Create a ws connection inside useEffect
and close on the effect cleanup would help or remove StrictMode
from index
file.
Thanks for pointing me to why. It was very helpful to work out why this was happening! This was very hard to debug otherwise. Should React show a warning? Or maybe WebSocket
should not create this second connection, just as console.log
does not log twice in Strict Mode.
This is bad because you can't abort a websocket which is in readyState==CONNECTING
. The component gets unmounted before the connection is fully established.
The unmount lambda then calls websocket.close()
making the first connection always break. This triggers the healthcheck and retry policies in my app which makes the app behave differently.
Frankly I am very perplexed on how the useEffect
+StrictMode
do not take in account this scenario.
Just encountered this issue. Tried several ways of tracking the re-render in hopes of creating a connection only on the second render, including trying to cache the connection in a different function but so far no luck. Also, I agree with @raffaeler, socket.close()
gets called way before the connection is even established.
Just spent hours debugging my websocket code. It worked fine in Chrome, but wasn't working in Firefox. Finally figured out that it was creating a connection, closing that and creating a second one. Lots more googling landed me here. Since I'm not seeing another way around this, I am disabling Strict Mode for now. At least I'll know in the future I can choose one or the other, not both at the same time.
I'd like to add that, what worked as a charm for me, and without having to remove the React.StrictMode
, was creating the WebSocket connection from a Middleware, and then concatenate it to my Redux flow 👍 💯
Sorry for the wild ping @raffaeler , have you found a way to fix your issue? I'm having the same here and since there's no activity anymore, I was wondering if you managed to :).
Thanks for your help
You are welcome @mfrachet
The quick and dirty solution is to remove entirely the React.StrictMode
tag from your entry point app file.
Apparently Meta does not care at all about the issues. The issues in this repository are dropped sistematically.
The right way of ensuring double WebSocket connections are not created is to track it as a global variable attached to the window
object. Since you must have only 1 connection to your backend anyways (for that tab that is), there is no need to create a new connection in useEffect
and attach it to your ref every time component is remounted. Instead, you can check if an existing connection is already bound to a global variable and if it is, then reuse that connection. Also should be checking readyState
to see if it was CLOSED
or CLOSING
when previous component was unmounted. If it was, then it should recreate the connection and rebind the global variable. Here is an example:
const client = useRef<WebSocket | null>(null)
useEffect(() => {
if (!(window.__webSocketClient instanceof WebSocket)) {
window.__webSocketClient = new WebSocket("ws://your/url/to/socket")
} else if (
window.__webSocketClient.readyState === WebSocket.CLOSED ||
window.__webSocketClient.readyState === WebSocket.CLOSING
) {
window.__webSocketClient = new WebSocket("ws://your/url/to/socket")
}
client.current = window.__webSocketClient
const message = (event: MessageEvent<any>) => {
console.log("MESSAGE FROM SERVER:", event.data)
}
client.current.onopen = () => {
console.log("ON OPEN CALLED ONLY ONCE")
client.current?.addEventListener("message", message)
setTimeout(() => {
if (client.current?.readyState === WebSocket.OPEN) {
console.log("SENDING A MESSAGE", client.current.readyState)
client.current?.send(`Hello from client!`)
}
}, 5000)
}
return () => {
console.log("CLEAN UP LISTENER!")
client.current?.removeEventListener("message", message)
if (client.current?.readyState === WebSocket.OPEN) {
console.log("closing socket!")
client.current?.close()
}
}
}, [])
Types for global variable:
declare global {
interface Window {
__webSocketClient: WebSocket
}
}
@shripadk I appreciate the effort, but frankly this lifecycle should be provided in the box with a dedicated hook (or a boolean flag inside useEffect. Please note that Websocket is just a good example, but certainly not the only one requiring a lifecycle tied to the opened window. Thank you anyway.
If someone still has this issue, found a solution that worked for me: https://stackoverflow.com/questions/12487828/what-does-websocket-is-closed-before-the-connection-is-established-mean
If someone still has this issue, found a solution that worked for me: https://stackoverflow.com/questions/12487828/what-does-websocket-is-closed-before-the-connection-is-established-mean
I don't see how this could resolve the duplicate connection issue. The answer about react double connection is clearly marked as bugged/wrong.
I don't know if this would help anyone but I tried to solve it by adding an event listener to close the websocket when cleaning up useEffect after the websocket open event.
useEffect(() => {
const ws = new WebSocket(url)
return () => {
if (ws.readyState === 1) {
ws.close()
} else {
ws.addEventListener('open', () => {
ws.close()
})
}
}
}, [])