Skip to content

callback triggered while dash.callback_contract.triggered is [{'prop_id': '.', 'value': None}] #1523

Description

@sdementen

Describe your context

I am trying to use a Store that could be written by multiple callbacks (to circumvent the 1 output can only be updated by one callback). The logic (hack?) looks sound and works except that I get a problem with a callback that is triggered with
dash.callback_contract.triggered == [{'prop_id': '.', 'value': None}] each time an unrelated Div is updated (ie not only on the first firing at the beginning of the app) and is moreover triggered twice.

Here is the sample app with the get_multi_store function generating a Div with multiple stores (one core and one per writer).

importdashimportdash_core_componentsasdccimportdash_html_componentsashtmlfromdash.dependenciesimportInput, Output, Stateapp=dash.Dash()
defget_multi_store(id_store, id_writers, mode="replace"):
"""Generate a store that can be written by multiple writers. For input, use Input(id_store, "data"). For output by writer 'x', use Output(id_store+"-x", "data"). If mode=="replace", the data of the store is replaced when written to it. If mode=="append", the data of the store is appended with the new data. """assertmodein {"replace", "append"}
N=len(id_writers)
id_container=f"{id_store}-container"id_store_root=id_storeid_store_writers= [f"{id_store}-{id_writer}"forid_writerinid_writers]
defget_layout(data):
"""Generate the layout with the root read-only store and the writable stores"""return [dcc.Store(id=id_store_root, data=data)] + [
dcc.Store(id=id_store_writer) forid_store_writerinid_store_writers
]
@app.callback(Output(id_container, "children"), [Input(id_store_writer, "modified_timestamp") forid_store_writerinid_store_writers], [State(id_store_writer, "data") forid_store_writerinid_store_writers]+ [State(id_store_root, "data")], )defupdate_stores(*args):
"""Callback to handle the update of the root store based on the writer stores"""# process args to retrieve the different input/state components# extract data from root store*args, data_root=args# reorganise stores data as [(ts1, data1), ...]stores_info=list(zip(args[:N], args[N:]))
# keep stores with timestamp defined and sort them by the timestampstores_info=sorted(
((ts, data) for (ts, data) instores_infoiftsisnotNone), key=lambdaitem: item[0]
)
# print(stores_info)# print(dash.callback_context.triggered)ifnotstores_info:
# no store updatedreturndash.no_update# in function of mode, update the data_rootifmode=="replace":
# replace the data_root by the more recent data store writets, new_data=stores_info[-1]
asserttsifnew_data==data_root:
returndash.no_updatedata_root=new_dataelse:
# append to data_root all data store writes (chronologically)data_root.extend(dataforts, datainstores_infoifts)
# rerender the layout with the new datalayout=get_layout(data_root)
returnlayoutinitial_data=Noneifmode=="replace"else []
layout=html.Div(id=id_container, children=get_layout(initial_data))
returnlayoutapp.layout=html.Div(
[
dcc.Input(id="a", value="0", type="number"),
dcc.Input(id="b", value="0", type="number"),
dcc.Input(id="c", value="0", type="number"),
html.Div(id="output-store"),
get_multi_store("scenario", ["a", "b", "c"], mode="append"),
]
)
# display output store (for debugging purposes)@app.callback(Output(component_id="output-store", component_property="children"), [Input(component_id="scenario", component_property="data")],)defdisplay_store(c):
returnstr(c)
# update a with b@app.callback(Output(component_id="scenario-c", component_property="data"), [Input(component_id="c", component_property="value")],)defset_c(c):
return {"c": c}
# update store with b@app.callback(Output(component_id="scenario-b", component_property="data"), [Input(component_id="b", component_property="value")],)defset_b(b):
return {"b": b}
# update store with a and update c (which triggers and update of store with c)@app.callback( [Output(component_id="c", component_property="value"),Output(component_id="scenario-a", component_property="data"), ], [Input(component_id="a", component_property="value")],)defset_a_and_c(a):
print(dash.callback_context.triggered)
# prints [{'prop_id': '.', 'value': None}]returnfloat(a) /2, {"a": a}
if__name__=="__main__":
app.run_server(debug=True)
  • replace the result of pip list | grep dash below
dash 1.18.1
dash-core-components 1.14.1
dash-html-components 1.1.1
dash-renderer 1.8.3
  • if frontend related, tell us your Browser, Version and OS

    • OS: windows 10
    • Browser chrome
    • Version: 87

Describe the bug

After the initialization of the app, the Store "scenario" holds the value:
[{'a': '0'}, {'b': '0'}, {'c': 0}]
When increasing the Input "a", the store changes to (as the input 'a' and the input 'c' are successively changed)
[{'a': '0'}, {'b': '0'}, {'c': 0}, {'a': 1}, {'c': 0.5}]
However, I see that the callback set_a_and_c is triggered once when the input 'a' is changed (OK) and twice afterwards with the line print(dash.callback_context.triggered) printing [{'prop_id': '.', 'value': None}]

Expected behavior

I would expect the callback set_a_and_c to be only triggered once with dash.callback_context.triggered==[{'prop_id': 'a.value', 'value': 1}].
Moreover, if I change the input 'c', I also see that the callback set_a_and_c is called twice with the dash.callback_context.triggered==[{'prop_id': 'a.value', 'value': 1}].

If I update the input 'b', then the callback set_a_and_c is called once with the dash.callback_context.triggered==[{'prop_id': 'a.value', 'value': 1}].

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3backlogbugsomething broken

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions