Clientside Callback (JS) appears to result in different behavior from seemingly equivalent Dash/Server Callback (Python)
geophpherie opened this issue · 3 comments
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?
Screen.Recording.2023-11-21.at.2.19.56.PM.mov
This second screen cap is the "expected" behavior using the serverside callback - the state does not update when preventUpdate
is in effect.
Screen.Recording.2023-11-21.at.2.31.36.PM.mov
Hopefully enough of that is ... somewhat clear?
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.
@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!
You may have accidentally found a way to solve the issue for rowData
persistence. XD