create-react-app icon indicating copy to clipboard operation
create-react-app copied to clipboard

Duplicate Websocket connection only in dev mode

Open chetbox opened this issue 3 years ago • 20 comments

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.

Screenshot from 2021-01-14 16-27-45

Reproducible demo

https://github.com/chetbox/react-dev-websocket-bug

chetbox avatar Jan 14 '21 16:01 chetbox

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.

n3tr avatar Jan 18 '21 09:01 n3tr

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.

chetbox avatar Jan 29 '21 09:01 chetbox

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.

raffaeler avatar Jun 26 '22 09:06 raffaeler

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.

MariaTheCoder avatar Jul 16 '22 16:07 MariaTheCoder

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.

mdodge-ecgrow avatar Sep 09 '22 21:09 mdodge-ecgrow

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 👍 💯

CxrlosKenobi avatar Oct 10 '22 22:10 CxrlosKenobi

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

mfrachet avatar Jan 02 '23 12:01 mfrachet

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.

raffaeler avatar Jan 02 '23 12:01 raffaeler

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 avatar Mar 23 '23 08:03 shripadk

@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.

raffaeler avatar Mar 23 '23 09:03 raffaeler

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

luklearn avatar Aug 03 '23 21:08 luklearn

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.

raffaeler avatar Aug 04 '23 06:08 raffaeler

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()
      })
    }
  }
}, [])

LcYxT avatar Aug 09 '23 10:08 LcYxT