pythonreactjsflasksocket.ioflask-socketio

Using sockets in react+flask application


I'm trying to implement sockets in my React+Flask application and am running into issues. When I start my backend server, I get a continuous loop of users connect. Then when I start the frontend, the users get disconnected. Any ideas on what the issue is? I was using this guide as a starting point, but no differences in the code are jumping out at me. The only difference between what I'm doing is I'm using Vite instead of Create React App.

Python packages

bidict==0.22.1
blinker==1.6.2
click==8.1.6
dnspython==2.4.1
Flask==2.3.2
Flask-Cors==4.0.0
Flask-SocketIO==5.3.5
greenlet==2.0.2
gunicorn==21.2.0
h11==0.14.0
importlib-metadata==6.8.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.3
packaging==23.1
pyserial==3.5
python-dotenv==1.0.0
python-engineio==4.5.1
python-socketio==5.8.0
simple-websocket==0.10.1
six==1.16.0
Werkzeug==2.3.6
wsproto==1.2.0
zipp==3.16.2

app.py

from flask import Flask, request
from flask_cors import CORS
from flask_socketio import SocketIO, emit


app = Flask(__name__)
CORS(app, resources={r"*": {"origins": "*"}})
socketio = SocketIO(app, cors_allowed_origins='*')


@socketio.on("connect")
def connected():
    """event listener when client connects to the server"""
    print('-' * 25)
    print(f"Client has connected: {request.sid}")
    emit("connect", {"data": f"id: {request.sid} is connected"})
    print('-' * 25)


@socketio.on("disconnect")
def disconnected():
    """event listener when client disconnects to the server"""
    print('-' * 25)
    print("User disconnected")
    emit("disconnect", f"User {request.sid} disconnected", broadcast=True)
    print('-' * 25)


if __name__ == "__main__":
    socketio.run(app, debug=True)

package.json

{
  "name": "socket-example",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "@reduxjs/toolkit": "^1.9.5",
    "@types/react-redux": "^7.1.25",
    "framer-motion": "^10.12.22",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-hot-toast": "^2.4.1",
    "react-icons": "^4.10.1",
    "react-redux": "^8.1.1",
    "socket.io": "^4.7.1",
    "socket.io-client": "^4.7.1",
    "uuid": "^9.0.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.14",
    "@types/react-dom": "^18.2.6",
    "@types/uuid": "^9.0.2",
    "@typescript-eslint/eslint-plugin": "^5.61.0",
    "@typescript-eslint/parser": "^5.61.0",
    "@vitejs/plugin-react": "^4.0.1",
    "autoprefixer": "^10.4.14",
    "eslint": "^8.44.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.1",
    "postcss": "^8.4.26",
    "prettier": "3.0.0",
    "tailwindcss": "^3.3.3",
    "typescript": "^5.0.2",
    "vite": "^4.4.0"
  }
}

App.tsx

import { useState, useEffect } from 'react'
import { io } from 'socket.io-client'

// "undefined" means the URL will be computed from the `window.location` object
const URL =
  import.meta.env.MODE === 'production' ? undefined : 'http://localhost:5000'

const socket = io(URL, {
  cors: {
    origin: 'http://localhost:5173',
  },
})

export default function App() {
  const [isConnected, setIsConnected] = useState(socket.connected)

  useEffect(() => {
    socket.on('connect', (data) => {
      console.log(data)
      setIsConnected(true)
    })

    socket.on('disconnect', (data) => {
      console.log(data)
      setIsConnected(false)
    })

    return () => {
      socket.disconnect()
    }
  }, [])

  return (
    <>
      <p>WebSocket: {'' + isConnected.toString()}</p>
    </>
  )
}

Solution

  • The problem can be narrowed down to this line:

    app.py

    @socketio.on("connect")
    def connected():
        """event listener when client connects to the server"""
        print('-' * 25)
        print(f"Client has connected: {request.sid}")
        emit("connect", {"data": f"id: {request.sid} is connected"}) # <- HERE
        print('-' * 25)
    

    Look closely at emit(event, args). You're emitting a "connect" event which is one of the reserved events by Socket.io.

    So what's happening is that the client's useEffect() is emitting a successful connect event to the server's @socketio.on("connect"). When we reach:

    emit("connect", {"data": f"id: {request.sid} is connected"})
    

    the server then emits a reserved "connect" event back to the client. The client receives the event, thinks a new socket has been successfully connected, and thus emits to the server AGAIN creating an infinite loop.

    This is why it looks like you're receiving a "continuous loop of user connects". In actuality, this is the same exact socket connection but with a new, refreshed response.sid for every looped client connect emission

    Similarly, your continuous client socket disconnects is caused from the server emission sending data which was not expected. If you update your client's disconnect handler with:

    socket.on("disconnect", (reason, details) => {
        console.log("disconnected:", reason, details);
    });
    

    you would get "parse error" and "invalid payload" for the reason and details regarding the disconnect. Since every subsequent client event handler is throwing an error, they all show up as a socket disconnect in your console's output

    A quick fix would be to update your "connect" keyword with something different:

    # "connect" becomes "connected"
    emit("connected", {"data": f"id: {request.sid} is connected"})