Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Updating options of a dropdown triggers update on value of the dropdown for pattern-matching callbacks #2487

Closed
ltsimple opened this issue Mar 31, 2023 · 4 comments · Fixed by #2548

Comments

@ltsimple
Copy link

If there are two pattern-matching callbacks for a dropdowns' options and value, the callback that changes the dropdown's options also triggers the other callback that uses the dropdown's value.

import dash
from dash import dcc, html, Input, Output, MATCH, ALL
import random

app = dash.Dash(__name__)
app.layout = html.Div([
    dcc.Dropdown(id="d1", options=[1, 2, 3]),
    dcc.Dropdown(id={'type': 'd2', "id": 1}, options=[]),
    dcc.Dropdown(id={'type': 'd2', "id": 2}, options=[]),
    html.Pre(id={"type": 'out', "id": 1}, style={'margin-top': '100px'}),
    html.Pre(id={"type": 'out', "id": 2}, style={'margin-top': '100px'})
])


@app.callback(
    Output({"type": 'd2', "id": ALL}, 'options'),
    Input('d1', 'value'),
    prevent_initial_call=True
)
def update_options(val):
    return [{
        1: ['a', 'b'],
        2: ['A', 'B'],
        3: ['x', 'y'],
    }[val] for _ in range(2)]


@app.callback(
    Output({"type": 'out', "id": MATCH}, 'children'),
    Input({"type": 'd2', "id": MATCH}, 'value'),
    prevent_initial_call=True
)
def update(_):
    return f'got_triggered_{random.randint(1, 10000)}'


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

In the example above, when I use the dropdown 'd1' to change the options of 'd2's, the first callback triggers as expected but after that second callback also triggers where it is not supposed to.

pip list

  • asyncstdlib 3.10.6
  • boto3 1.26.96
  • botocore 1.29.96
  • Brotli 1.0.9
  • cachelib 0.9.0
  • cachetools 5.3.0
  • certifi 2022.12.7
  • charset-normalizer 3.1.0
  • click 8.1.3
  • colorama 0.4.6
  • confuse 2.0.0
  • Cython 0.29.33
  • dash 2.9.2
  • dash-bootstrap-components 1.4.1
  • dash-core-components 2.0.0
  • dash-html-components 2.0.0
  • dash-table 5.0.0
  • dnspython 2.3.0
  • EditorConfig 0.12.3
  • Flask 2.2.3
  • Flask-Caching 2.0.2
  • Flask-Compress 1.13
  • gunicorn 20.1.0
  • idna 3.4
  • itsdangerous 2.1.2
  • Jinja2 3.1.2
  • jmespath 1.0.1
  • jsbeautifier 1.14.7
  • MarkupSafe 2.1.2
  • more-itertools 9.1.0
  • numpy 1.24.2
  • pandas 1.5.3
  • pip 23.0.1
  • plotly 5.13.1
  • pymongo 4.3.3
  • python-amazon-sp-api 0.18.2
  • python-dateutil 2.8.2
  • python-dotenv 1.0.0
  • pytz 2023.2
  • PyYAML 6.0
  • requests 2.28.2
  • s3transfer 0.6.0
  • setuptools 65.5.0
  • six 1.16.0
  • tenacity 8.2.2
  • urllib3 1.26.15
  • Werkzeug 2.2.3
@HHarald99
Copy link

HHarald99 commented Apr 5, 2023

Is this really about pattern matching? After my latest dash update I generally have the problem that updates of "options" will call the "value" callback of a dropdown. This seems to happen since version 2.7.1. Example:

import dash
from dash import dcc, html, Input, Output

app = dash.Dash(__name__)
app.layout = html.Div([
    html.Button("button", id="button"),
    dcc.Dropdown(id="dropdown"),
    html.Div(id="dummy"),
])

@app.callback(
    Output("dropdown", "options"),
    Input('button', 'n_clicks'),
    prevent_initial_call=True
)
def update_options(n_clicks):
    return [n_clicks]

@app.callback(
    Output("dummy", "children"),
    Input("dropdown", "value"),
    prevent_initial_call=True
)
def value_selected(value):
    print("triggered", value)


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

In dash 2.7.0 the button will not trigger the second callback, since 2.7.1 it will.

@ltsimple
Copy link
Author

ltsimple commented Apr 5, 2023

While I was trying older versions of Dash, this bug sometimes happened but sometimes didn't in version 2.7.0 and I just couldn't figure it out why but never happened with normal callbacks. Could it be something about dependencies? Because when you try to install an older version of Dash, pip uninstall only Dash then installs the version you choose but keeps the dependencies the same.

@T4rk1n
Copy link
Contributor

T4rk1n commented Apr 5, 2023

Thanks for the report, I've been trying to fix the regression with the component as prop (Dropdown.options is a component as prop) triggering other callbacks not on the prop that changed, it started when I changed the callback results to trigger callbacks on components returned by callbacks in 2.7.1 and there was a couple fixes over the last few versions.

Normal callbacks are fixed and tested in

def test_rdcap003_side_effect_regression(dash_duo):

I tested the code provided here and can reproduce, I further isolated the callbacks and seems like this is another case involving Output and pattern matching ids, those get triggered by normal & pattern matching inputs.

@sergiykhan
Copy link

sergiykhan commented Apr 11, 2023

I am adding my own example (#2452) to demonstrate the issue. There is no pattern-matching here. The video recording shows how the unrelated trigger (a button) gets fired by me typing in the drop-down menu. I confirm that 2.7.0 works as expected.

dash==2.9.2
dash-bootstrap-components==1.3.1
dash-core-components==2.0.0
dash-html-components==2.0.0
dash-table==5.0.0
screen-capture.webm
from dash import Dash, dcc, html, dcc, Input, Output, ctx
from dash.exceptions import PreventUpdate

options = [
    {"label": "aa1", "value": "aa1"},
    {"label": "aa2", "value": "aa2"},
    {"label": "aa3", "value": "aa3"},
    {"label": "best value", "value": "bb1"},
    {"label": "better value", "value": "bb2"},
    {"label": "bye", "value": "bb3"},
]

app = Dash(__name__)
app.layout = html.Div([
    html.Div([
        "Single dynamic Dropdown",
        dcc.Dropdown(id="my-dynamic-dropdown")
    ], style={'width': 200, 'marginLeft': 20, 'marginTop': 20}),
    html.Button(
        'Reset',
        id='button',
        n_clicks=0,
    ),
])


@app.callback(
    Output("my-dynamic-dropdown", "options"),
    Input("my-dynamic-dropdown", "search_value")
)
def update_options(search_value):
    if not search_value:
        raise PreventUpdate
    return [o for o in options if search_value in o["label"]]


@app.callback(
    Output('my-dynamic-dropdown', 'value'),
    Input('button', 'n_clicks'),
)
def on_button(n_clicks):
    print('Button pressed', n_clicks, ctx.triggered_id)
    return None


if __name__ == "__main__":
    app.run_server(debug=True, port=5000)

Output:

Button pressed 0 None
Button pressed 0 None
Button pressed 0 None
Button pressed 0 None
Button pressed 0 None
Button pressed 0 None
Button pressed 0 None

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants