[BUG] Callbacks triggered unnecessarily with MATCH (pattern-matching callbacks)
Describe your context
dash 1.20.0
dash-bootstrap-components 0.12.2
dash-core-components 1.16.0
dash-html-components 1.1.3
dash-renderer 1.9.1
dash-table 4.11.3
Describe the bug
In the example below, the callback using MATCH is triggered for all matching components, even ones that haven't changed and don't need updating.
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, MATCH, ALL
app = dash.Dash(__name__, suppress_callback_exceptions=True)
app.layout = html.Div(
[
html.Button("Add Filter", id="dynamic-add-filter", n_clicks=0),
html.Div(id="dynamic-dropdown-container", children=[]),
]
)
@app.callback(
Output("dynamic-dropdown-container", "children"),
Input("dynamic-add-filter", "n_clicks"),
State("dynamic-dropdown-container", "children"),
)
def display_dropdowns(n_clicks, children):
new_element = html.Div(
[
dcc.Dropdown(
id={
"type": "dynamic-dropdown",
"index": n_clicks,
},
options=[
{"label": i, "value": i} for i in ["NYC", "MTL", "LA", "TOKYO"]
],
style={"width": "200px", "display": "inline-block"},
),
dcc.Input(
id={
"type": "dynamic-input",
"index": n_clicks,
},
type="number",
style={"width": "200px", "display": "inline-block"},
),
html.Div(
id={
"type": "dynamic-output",
"index": n_clicks,
}
),
]
)
children.append(new_element)
return children
@app.callback(
Output({"type": "dynamic-input", "index": MATCH}, "value"),
Input({"type": "dynamic-dropdown", "index": MATCH}, "value"),
)
def set_initial_value(city):
values = {
"NYC": 10,
"MTL": 20,
"LA": 30,
"TOKYO": 40,
None: None,
}
return values[city]
@app.callback(
Output({"type": "dynamic-output", "index": MATCH}, "children"),
Input({"type": "dynamic-dropdown", "index": MATCH}, "value"),
State({"type": "dynamic-dropdown", "index": MATCH}, "id"),
)
def display_output(value, id):
return html.Div("Dropdown {} = {}".format(id["index"], value))
if __name__ == "__main__":
app.run_server(debug=True)
Example:
- Select a value from the dropdown. The input next to it is populated with an initial value
- Change that value (e.g. just increment it by 1)
- Click "Add Filter". The new row is added correctly, but the first input is reset to its initial value even though the first dropdown has not changed.
Expected behavior
The set_initial_value function should only be called for components that have changed. It seems to be called for all matching components even if only one of them has changed.
You can prevent the initial update with the prevent_initial_call parameter.
Works fine with this code:
@app.callback(
Output({"type": "dynamic-input", "index": MATCH}, "value"),
Input({"type": "dynamic-dropdown", "index": MATCH}, "value"),
prevent_initial_call=True,
)
def set_initial_value(city):
values = {
"NYC": 10,
"MTL": 20,
"LA": 30,
"TOKYO": 40,
None: None,
}
return values[city]
prevent_initial_call only prevents callbacks at the start of the app. If components are dynamically added and if they match the callback, then the callback will be triggered. My assumption is that whatever prevents the call during startup does not occur after the app has loaded. Basically, going from a none state (or undefined) to a component value is registered as a change rather than the change occurring because of an event trigger.
Hi - we are tidying up stale issues and PRs in Plotly's public repositories so that we can focus on things that are most important to our community. If this issue is still a concern, please add a comment letting us know what recent version of our software you've checked it with so that I can reopen it and add it to our backlog. (Please note that we will give priority to reports that include a short reproducible example.) If you'd like to submit a PR, we'd be happy to prioritize a review, and if it's a request for tech support, please post in our community forum. Thank you - @gvwilson
This is still an issue using the same code and text as above: " Describe your context
dash==2.17.0
dash_core_components==2.0.0
dash_html_components==2.0.0
Describe the bug
In the example below, the callback using MATCH is triggered for all matching components, even ones that haven't changed and don't need updating.
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, MATCH, ALL
app = dash.Dash(__name__, suppress_callback_exceptions=True)
app.layout = html.Div(
[
html.Button("Add Filter", id="dynamic-add-filter", n_clicks=0),
html.Div(id="dynamic-dropdown-container", children=[]),
]
)
@app.callback(
Output("dynamic-dropdown-container", "children"),
Input("dynamic-add-filter", "n_clicks"),
State("dynamic-dropdown-container", "children"),
)
def display_dropdowns(n_clicks, children):
new_element = html.Div(
[
dcc.Dropdown(
id={
"type": "dynamic-dropdown",
"index": n_clicks,
},
options=[
{"label": i, "value": i} for i in ["NYC", "MTL", "LA", "TOKYO"]
],
style={"width": "200px", "display": "inline-block"},
),
dcc.Input(
id={
"type": "dynamic-input",
"index": n_clicks,
},
type="number",
style={"width": "200px", "display": "inline-block"},
),
html.Div(
id={
"type": "dynamic-output",
"index": n_clicks,
}
),
]
)
children.append(new_element)
return children
@app.callback(
Output({"type": "dynamic-input", "index": MATCH}, "value"),
Input({"type": "dynamic-dropdown", "index": MATCH}, "value"),
)
def set_initial_value(city):
values = {
"NYC": 10,
"MTL": 20,
"LA": 30,
"TOKYO": 40,
None: None,
}
return values[city]
@app.callback(
Output({"type": "dynamic-output", "index": MATCH}, "children"),
Input({"type": "dynamic-dropdown", "index": MATCH}, "value"),
State({"type": "dynamic-dropdown", "index": MATCH}, "id"),
)
def display_output(value, id):
return html.Div("Dropdown {} = {}".format(id["index"], value))
if __name__ == "__main__":
app.run_server(debug=True)
Example:
Select a value from the dropdown. The input next to it is populated with an initial value Change that value (e.g. just increment it by 1) Click "Add Filter". The new row is added correctly, but the first input is reset to its initial value even though the first dropdown has not changed.
Expected behavior
The set_initial_value function should only be called for components that have changed. It seems to be called for all matching components even if only one of them has changed. "
Although this can be avoided by using prevent_initial_call=True in the callback, I believe that this is still unexpected behavior.
There may also be a relation to Issue #2371 as well, in which a similar confusion around pattern-matching callbacks can be found. (https://github.com/plotly/dash/issues/2371)
@gvwilson Will this issue be reopened? It is still apparent on Dash 2.18.2.
@T4rk1n when you have time can you please check if this issue exists in Dash 3.0?
@gvwilson, I tested it with Dash 3.0, and the issue persists. Using the code provided by @kevin-gett to print the callback trigger, I observed that adding a new element triggers all existing elements with an ID of None and props {}
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, MATCH, ALL
app = dash.Dash(__name__, suppress_callback_exceptions=True)
app.layout = html.Div(
[
html.Button("Add Filter", id="dynamic-add-filter", n_clicks=0),
html.Div(id="dynamic-dropdown-container", children=[]),
]
)
@app.callback(
Output("dynamic-dropdown-container", "children"),
Input("dynamic-add-filter", "n_clicks"),
State("dynamic-dropdown-container", "children"),
)
def display_dropdowns(n_clicks, children):
new_element = html.Div(
[
dcc.Dropdown(
id={
"type": "dynamic-dropdown",
"index": n_clicks,
},
options=[
{"label": i, "value": i} for i in ["NYC", "MTL", "LA", "TOKYO"]
],
style={"width": "200px", "display": "inline-block"},
),
dcc.Input(
id={
"type": "dynamic-input",
"index": n_clicks,
},
type="number",
style={"width": "200px", "display": "inline-block"},
),
html.Div(
id={
"type": "dynamic-output",
"index": n_clicks,
}
),
]
)
children.append(new_element)
return children
@app.callback(
Output({"type": "dynamic-input", "index": MATCH}, "value"),
Input({"type": "dynamic-dropdown", "index": MATCH}, "value"),
)
def set_initial_value(city):
print(dash.callback_context.triggered_id)
print(dash.callback_context.triggered_prop_ids)
values = {
"NYC": 10,
"MTL": 20,
"LA": 30,
"TOKYO": 40,
None: None,
}
return values[city]
@app.callback(
Output({"type": "dynamic-output", "index": MATCH}, "children"),
Input({"type": "dynamic-dropdown", "index": MATCH}, "value"),
State({"type": "dynamic-dropdown", "index": MATCH}, "id"),
)
def display_output(value, id):
return html.Div("Dropdown {} = {}".format(id["index"], value))
if __name__ == "__main__":
app.run(debug=True)
Is there a suggested workaround for this problem?