New websocket component: DashSocketIO

I don’t understand what you mean by the messages being dropped.

Client side is on the user’s browser, server side is wherever your Dash app is running.

I’m not a front end developer but this looks very promising.

Can it be used as a drop in replacement for the Websocket dash extension?

I made a dashboard with Websocket but found that it did not work when deployed on a server.

Really interesting!
I see some opportunities to leverage this for streaming data from my PostgreSQL database, using cursors. Which would be really more efficient

Hello,

I have a question regarding the sockets.

I have an async server with socket and my flask/dash server with DashSocketIO running with Nginx. The UI uses two DashSocketIO, to connect to each of the servers.

Since I have two socket endpoints and nginx, I need to distinguish which connection DashSocketIo is. I tried to set the URL to the base url of my server including an ending such as /socket_ui. But this won’t work.

I saw that there is a path variable with socketIO which would do the trick. This is not exposed by DashSocketIo. Is there any other way to route the socket connections correctly to the right servers?

Or maybe extend the component

const DashSocketIO = (props: Props) => {
   const { url, path, setProps, eventNames } = props;
   const socket = React.useMemo(() => io(
     url || undefined, {
       auth: {pathname: window.location.pathname},
       path: path || '/socket.io'
     }
   ), [url, path])

Is that an option and would work?

Thank you

Hey @simon-u, yes definitely can add more of the socketio client options in there. Let me know if you want to make a contribution to the code, otherwise I’ll try to have a look in the coming weeks :slight_smile:

1 Like

I made a pull request.

In my application, its working with nginx. They are some build files, you want to remove them?

Thanks for this useful package @RenaudLN.

When I deploy my app that uses this package on posit, the GET requests fail because they seem the be routed to a wrong address (e.g. <SERVER/socket.io>) because the prefix is not included in the address; i.e. the address should be <SERVER/GUID/socket.io) where GUID is the unique identifier of the container deployed on posit.

I tried to overcome that error by setting the path argument when I instantiate SocketIO but it still seem to be routing the GET to the original wrong address.

I tried setting the url arg of DashSocketIO but for some reason it drops some part of the GUID, i.e. instead of routing to SERVER/connect/xyz/socket.io, it routes to SERVER/connect/socket.io – xyz is dropped.

@SaberTooth

I had the same issue, I made a pull request to include the path argument into DashSocketIO.
This will allow you to set the container id as part.

I still need to make some changes. Hopefully, I find the time today/tomorrow.

1 Like

Thanks @simon-u. That’d be great.

Hello,
Such a great job, thank you, @RenaudLN !
Have anyone tried this component with Celery+Redis? In other words, how to emit a message from a celery task? Is it even possible? From what i understood, there is no dedicated socket server and thus not clear how to define socketio.Client()
Here is my trivial usecase:

  • user start time-consuming report processing (background celery workers)
  • while processing the user continue interacting with app
  • once report is ready, notify the user emitting back a message
    Any help is greatly appreciated

Thanks for the great package @RenaudLN.

I tried to use it to update an AgGrid but the clientside callback seems to loose lines if called again too soon.

Server log shows all the rows being emmited:

Emitting row: {'Name': 'Alice', 'Age': 24, 'City': 'New York'} to socket_id: sJe_c5voiflZsfVvAAAF
Emitting row: {'Name': 'Bob', 'Age': 27, 'City': 'San Francisco'} to socket_id: sJe_c5voiflZsfVvAAAF
Emitting row: {'Name': 'Charlie', 'Age': 22, 'City': 'London'} to socket_id: sJe_c5voiflZsfVvAAAF
Emitting row: {'Name': 'Diana', 'Age': 32, 'City': 'Berlin'} to socket_id: sJe_c5voiflZsfVvAAAF

But only two rows are added to the grid:


This is definetly a timing issue as added a one second sleep between two lines updates fixes the problem.
Obviously this is not a true solution as my goal is to be able to update the table with batches of 50000 lines or more over the websocket.

Is there a way to ensure every line emited is received?

Here’s my full test code for reference:

from time import sleep

import dash
from dash import html, Input, callback, no_update, State, clientside_callback, Output, _dash_renderer
import dash_ag_grid as dag
from dash_socketio import DashSocketIO
from flask_socketio import SocketIO

# Sample data
data_rows = [
    {"Name": "Alice", "Age": 24, "City": "New York"},
    {"Name": "Bob", "Age": 27, "City": "San Francisco"},
    {"Name": "Charlie", "Age": 22, "City": "London"},
    {"Name": "Diana", "Age": 32, "City": "Berlin"},
]

# Define column definitions for AgGrid
column_defs = [
    {"headerName": "Name", "field": "Name"},
    {"headerName": "Age", "field": "Age"},
    {"headerName": "City", "field": "City"}
]

# Initialize Dash app
_dash_renderer._set_react_version("18.3.1")
dash_app = dash.Dash(__name__, assets_folder="assets")
dash_app.server.secret_key = "Test!"
socketio = SocketIO(dash_app.server)

dash_app.layout = html.Div([
    html.H2("Simple Dash App with AgGrid"),
    dag.AgGrid(
        id="my-grid",
        rowData=[],
        columnDefs=column_defs,
        columnSize="sizeToFit",  # Makes columns auto-fit width
        defaultColDef={"sortable": True, "filter": True, "resizable": True},
        style={"height": "400px", "width": "100%"},
    ),
    html.Button("Load Data", id="load-data-btn"),
    DashSocketIO(id="grid_socket", eventNames=["gridLine"])
])


#### End of app initialization ####
@socketio.on("connect")
def on_connect():
    print("Client connected")

@socketio.on("disconnect")
def on_disconnect():
    print("Client disconnected")

@callback(
    Output("my-grid", "rowData", allow_duplicate=True),
    Input("load-data-btn", "n_clicks"),
    State("grid_socket", "socketId"),
    prevent_initial_call=True
)
def load_data(n_clicks, socket_id):
    if not n_clicks or not socket_id:
        print("No clicks or socket ID provided, skipping data load.")
        return no_update
    # Emit data to the grid via SocketIO
    for row in data_rows:
        print(f"Emitting row: {row} to socket_id: {socket_id}")
        socketio.emit("gridLine", [row], to=socket_id)
        # sleep(1)
    return no_update

# Active le bouton quand le socket est connecté
clientside_callback(
    """connected => !connected""",
    Output("load-data-btn", "disabled"),
    Input("grid_socket", "connected"),
)

clientside_callback(
    """(lines, grid) => {
    if (!lines) return dash_clientside.no_update;
    if (!grid) grid = [];
    console.log("Adding lines:", lines, "to grid:", grid);
    return Array.isArray(lines) ? [...grid, ...lines] : [...grid, lines];
    }""",
    Output("my-grid", "rowData"),
    Input("grid_socket", "data-gridLine"),
    State("my-grid", "rowData"),
    prevent_initial_call=True
)


if __name__ == "__main__":
    dash_app.run(debug=True)

Hey @Junn_Sorran given what you describe this doesn’t sound like an issue with the socketio library but rather how the client side callback/aggrid handle the emitted data.
I know there are row transactions with Ag grid, maybe you could try this instead of your own row concatenation?

Thanks for the quick answer @RenaudLN, the row concatenation might not be the best way to update the grid indeed but if you look at the server log and the console log you can see that half the clientside callback have never been executed anyway.

So i’m not sur changing the way they respond would solve the problem, unless there’s a way to update the gird without clientside callbacks. A way to send the callback output trought the websocket would be nice, but i’m not sure it’s possible?