Repository navigation
callback triggered while dash.callback_contract.triggered is [{'prop_id': '.', 'value': None}] #1523
Description
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).
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
app = dash.Dash()
def get_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.
"""
assert mode in {"replace", "append"}
N = len(id_writers)
id_container = f"{id_store}-container"
id_store_root = id_store
id_store_writers = [f"{id_store}-{id_writer}" for id_writer in id_writers]
def get_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) for id_store_writer in id_store_writers
]
@app.callback(
Output(id_container, "children"),
[Input(id_store_writer, "modified_timestamp") for id_store_writer in id_store_writers],
[State(id_store_writer, "data") for id_store_writer in id_store_writers]
+ [State(id_store_root, "data")],
)
def update_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 timestamp
stores_info = sorted(
((ts, data) for (ts, data) in stores_info if ts is not None), key=lambda item: item[0]
)
# print(stores_info)
# print(dash.callback_context.triggered)
if not stores_info:
# no store updated
return dash.no_update
# in function of mode, update the data_root
if mode == "replace":
# replace the data_root by the more recent data store write
ts, new_data = stores_info[-1]
assert ts
if new_data == data_root:
return dash.no_update
data_root = new_data
else:
# append to data_root all data store writes (chronologically)
data_root.extend(data for ts, data in stores_info if ts)
# rerender the layout with the new data
layout = get_layout(data_root)
return layout
initial_data = None if mode == "replace" else []
layout = html.Div(id=id_container, children=get_layout(initial_data))
return layout
app.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")],
)
def display_store(c):
return str(c)
# update a with b
@app.callback(
Output(component_id="scenario-c", component_property="data"),
[Input(component_id="c", component_property="value")],
)
def set_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")],
)
def set_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")],
)
def set_a_and_c(a):
print(dash.callback_context.triggered)
# prints [{'prop_id': '.', 'value': None}]
return float(a) / 2, {"a": a}
if __name__ == "__main__":
app.run_server(debug=True)- replace the result of
pip list | grep dashbelow
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}].
Activity
sdementen commentedon Jan 12, 2021
To circumvent the issue, I could use the following decorator
on each of the callback but then I would miss the initial calls to the callback at the start of the app (AFAIK)
alexcjohnson commentedon Jan 12, 2021
Thanks @sdementen - possibly related to #1519
The
callback_contextyou're seeing is meant for the initial call of a callback where in most cases there is (according to the convention we adopted) no trigger for the callback. This value is there for backward compatibility with older versions of Dash that always reported exactly one triggering input. It doesn't make sense that this behavior is happening in this case though, so this is definitely a bug.While we're here I'll point out that instead of
if dash.callback_context.triggered == [{"prop_id": ".", "value": None}]you can sayif not dash.callback_context.triggeredbecause of the way we constructed it. That's a better pattern to use for new code because in a future major version of Dash we may take out this weird value and just use[]dash/dash/_callback_context.py
Lines 21 to 31 in 3402cb8
alexcjohnson commentedon Jan 12, 2021
Oh actually, I didn't fully understand what you were doing at first. You've found the loophole that effectively you can create a circular callback chain by having the output of a callback be its parent's
childrenprop. That means you're recreating the store components, thus their callbacks are triggered in a way that looks like an initial callback. I'm not sure why this is happening twice, but I don't think there's a lot we can do to avoid strange behavior in this case - if the values you create this way don't converge it's possible you'd trigger an infinite loop of callbacks.I'm not clear on why you need this to function like multiple callbacks all outputting to the same prop, as opposed to having all of those same inputs feeding into one callback, or at least having a separate callback combining them all into one final output. What about a clientside merge callback, like @nicolaskruchten describes in plotly/dash-core-components#881 (comment) ?
sdementen commentedon Jan 13, 2021
tx @alexcjohnson for your insight!
On my initial problem, I am indeed changing the parent container children so that it resets the writer stores. Hence, at any time, only the root store has content (as the writer stores have been reset) except when a callback is triggered by one of the writer stores. The writer stores are only used as
What I do not understand is that when I change the parent container, only the callbacks with the Stores in "input" should be triggered, right (but I have a doubt now...) ? and in my case, the Stores are only used as outputs. I would expect that, once the parent container is updated, no callback triggered except the one using the root Store as Input.
If this is not the case and that callbacks are triggered whenever an Input but also an Output element is created, then you can close the issue.
The documentation in https://dash.plotly.com/advanced-callbacks states that only callbacks which have as input the new component will be triggered
Regarding the client side callback, this is indeed an excellent idea/suggestion. It removes all "spurious" callbacks and make the whole more reactive. I will make a post on the plotly community when the details are ironed out
alexcjohnson commentedon Jan 13, 2021
Ah yes, that part of the advanced callbacks doc page is not quite correct.
If the new components added to the layout contain callback outputs, these callbacks also need to fire. This is similar to what happens on page load: we don't assume the component was created with the prop values it ultimately should have, we allow the callback to run and decide that.
There is some subtle logic in this case surrounding what props count as having triggered the callback - the key distinction being whether the entire callback (inputs and outputs at least, I'd have to look back to see where we landed on state) is contained within the new layout fragment. If so, it's treated EXACTLY like the initial page load with respect to
callback_context.triggeredie none of the inputs appear as triggers unless there's a chain and an input is itself an output of another callback. If any of the inputs/outputs is outside the new layout fragment, then the inputs within that fragment DO count as triggers. The initial discussion of this was here in the pattern-matching callbacks PR: #1103 (comment)cc @jdamiba - I missed this point when we were discussing these details, perhaps we can elaborate on this section? Most important is the point about newly-created outputs triggering callbacks, but we should sort out the details re:
triggersand document this as well.jdamiba commentedon Jan 18, 2021
@alexcjohnson I agree that it would be a good idea to update the section that covers this behavior in dash-docs. Send me an invite for a meeting so we can get on the same page :)
sdementen commentedon Jan 18, 2021
@jdamiba @alexcjohnson it would be nice, besides the update of this section of the doc, to have a documentation on the interactions between frontend/client and server with the logic around the callback graph, the format of the JSON send from the client to the server and vice-versa.
I am using django-plotly-dash (and sending some PR there) and it involves some reverse engineering to understand the protocol and assumptions used in Dash.
alexcjohnson commentedon Jan 19, 2021
Logic around the callback graph definitely, we should document fully how and when each callback is triggered in many different cases. @jdamiba has added all sorts of great info about this in https://dash.plotly.com/advanced-callbacks, "When Are Callbacks Executed?" as well as https://dash.plotly.com/app-lifecycle and he and I will take another pass through this at some point, but if there's still missing or incorrect info there please let us know (or make a PR 🏆 )
And I suppose given that we have multiple back ends, the client-server contract is relatively firm. It's not real high on our priority list but we'd happily include this in the docs if anyone is motivated to write it!
sdementen commentedon Jan 23, 2021
I would be interested in the documentation of the graph of dependencies on the client side and on the logic that trigger the callbacks in function of the graph events.
And this to understand better dash and tinker about possible evolutions to allow cycles in the graph (amuet avoid infinite loops), to sync components, to add the ability to trigger conditionally (i.e. if the object observed satisfies some conditions, then trigger the callback, otherwise do not), ...
[-][BUG] callback triggered while dash.callback_contract.triggered is [{'prop_id': '.', 'value': None}][/-][+]callback triggered while dash.callback_contract.triggered is [{'prop_id': '.', 'value': None}][/+]