plotly / dash-ag-grid

Dash AG Grid is a high-performance and highly customizable component that wraps AG Grid, designed for creating rich datagrids.
https://dash.plotly.com/dash-ag-grid
MIT License
175 stars 25 forks source link

Clientside Callback (JS) appears to result in different behavior from seemingly equivalent Dash/Server Callback (Python) #256

Closed geophpherie closed 11 months ago

geophpherie commented 11 months ago

Python 3.11.4, Dash 2.14.1, Dash-Ag-Grid 2.4.0

I'm posting here as I cannot seem to reproduce this behavior with a more or less equivalent set-up using dash-core-components so my initial thought is it might be related to ag-grid somehow.

The basic gist is this: I have a dcc.Store to handle my state and am pulling from it to update my grid's rowData. In my current example, I'm using a callback (server side) attached to cellValueChanged to update my state, then using the state to update rowData. [it's a bit circular for this trivial example, but in my actual use case I'm trying to draw/patch all my tables from a single source of truth in state where some computations might need to happen on cellValueChange before sending new data to the tables]. I'm experimenting with the update to rowData happening as either a client side callback or a server side one. However, i've stumbled into a situation where it seems as though they behave differently and in an unexpected fashion.

Now, please bear with me as it's a slightly weird scenario, but essentially, if I end up calling PreventUpdate from my cellValueChanged callback, when my rowData update callback is on the client, I still see updates to my state/localStorage (unexpected) whereas when it lives on the server, i do not (as expected).

The reason I felt this was worth to create an issue is that, with the only route to update state/localStorage being blocked by a PreventUpdate, I found it strange that the existence of this clientside callback let the state/localStorage still update.

Hopefully some code will help:

Here's a minimal example.

from dash import (
    dcc,
    html,
    Dash,
    callback,
    Input,
    Output,
    State,
    clientside_callback,
    no_update,
)
from dash.exceptions import PreventUpdate
import dash_ag_grid as dag

app = Dash(__name__)

columnDefs = [
    {
        "field": "direction",
        "editable": True,
    },
    {
        "field": "color",
        "editable": True,
    },
]

layout = html.Div(
    [
        html.Div(
            [
                dag.AgGrid(
                    id="grid",
                    rowData=[],
                    columnDefs=columnDefs,
                    style={"height": "200px"},
                )
            ]
        ),
        html.Div(id="output", children=["output here"]),
        dcc.Store(
            "state",
            data=[{"direction": "North", "color": "orange"}],
            storage_type="local",
        ),
        dcc.Checklist(id="prevent-toggle", options=["Prevent State Update"]),
    ]
)

app.layout = layout

@callback(
    output=Output(
        "state",
        "data",
    ),
    inputs=[Input("grid", "cellValueChanged"), State("prevent-toggle", "value")],
    prevent_initial_call=True,
)
def on_grid_cell_change(change_data, do_prevent_update):
    if do_prevent_update:
        print(f"Preventing Update Of: {change_data}")
        raise PreventUpdate
        # return no_update
    else:
        print(f"Updating:  {change_data}")
        return [change_data["data"]]

# keep this callback in to see expected behavior
# @callback(
#     output=Output("grid", "rowData"),
#     inputs=Input("state", "data"),
# )
# def add_rows_to_grid(row_data):
#     return row_data

# keep this callback in to see the error (when checking preventUpdate)
clientside_callback(
    """
    function addRowsToGrid(row_data) {
        console.log(row_data)
        return Object.values(row_data);
    }
    """,
    Output("grid", "rowData"),
    Input("state", "data"),
)

clientside_callback(
    """
    function printState(row_data) {
        return `STATE: ${JSON.stringify(row_data)}`;
    }
    """,
    Output("output", "children"),
    Input("state", "data"),
)

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

This first screen cap is the "unexpected" behavior using the clientside callback. You'll see how making changes in the table updates the Local Storage, as well as a separate clientside callback printing the storage values to a screen. However, when I enable preventUpdate, you'll see Local Storage still updates, however the clientside callback printing to screen doesn't. This just seems very odd to me, as 1) i expect that state should not be updated here and 2) since it is anyway, why is only one of the callbacks "seeing" it?

https://github.com/plotly/dash-ag-grid/assets/19380579/846f3953-46b6-48c0-9a9a-cf03e9cf6245

This second screen cap is the "expected" behavior using the serverside callback - the state does not update when preventUpdate is in effect.

https://github.com/plotly/dash-ag-grid/assets/19380579/f69a5249-a00a-4660-a7ae-90fbd2f114c9

Hopefully enough of that is ... somewhat clear?

BSd3v commented 11 months ago

Hello @jbeyer16 ,

I think this may be due to how you are setting the rowData with the store. In this fashion, it looks like you have actually linked them to be the same.

Try using JSON.parse(JSON.stringify(data)) as what you are returning to the rowData.

Also, I would highly recommend using rowTransactions vs resending the rowData back and forth each time. In order for rowTransactions to work, you'll need to have the getRowId set as a uniqueid from the data.

geophpherie commented 11 months ago

@BSd3v ooooooh great observation. thanks for pointing that out. indeed it seems they are referencing the same object, creating a copy as you suggest seems to now work as expected. Thanks for the second set of eyes there.

Indeed in my actual use case I have getRowId set as it did appear as the better way to go about things!

I appreciate the help here!

BSd3v commented 11 months ago

You may have accidentally found a way to solve the issue for rowData persistence. XD