Callbacks not working correctly in dash > 1.6.1

A few people seems to be having issues with callbacks in the recent versions of dash (specifically, newer than 1.6.1), myself included. I have created a MWE that should demonstrate the issue,

import dash
import dash_core_components as dcc
import dash_html_components as html

from dash.dependencies import Input, Output, State

# Data for selectors.
regions = ["DK1", "DK2"]
base_years = [2018, 2019]
model_index = {region: base_years for region in regions}
# Configuration keys.
ctrl_ids = [0, 1, 2]

# region Layout

option_map = {
    "region": [{"label": key, "value": key} for key in sorted(model_index.keys())],
    "base-year": [],
}


def layout():
    return html.Div(children=[html.Div([html.Div(make_config(ctrl_id)) for i, ctrl_id in enumerate(ctrl_ids)]),
                              html.Br(),
                              html.Div(html.Div("Content will be here."))])


def make_config(ctrl_id):
    children = [html.P("Case {}".format(ctrl_id))]
    for key in option_map.keys():
        inner_children = [dcc.Dropdown(id="dd-{}{}".format(key, ctrl_id), options=option_map[key])]
        # The first controller gets a lock button.
        if ctrl_id == 0:
            inner_children.append(html.Button(children="LOCK", id="btn-dd-{}{}".format(key, ctrl_id)))
        children.append(html.Div(children=inner_children))
    return html.Div(children=children)


# endregion

# region Callbacks

def register(app):
    def load_base_year(region):
        if region is None:
            return []
        return [{"label": year, "value": year} for year in model_index[region]]

    # Update available base year options on region change.
    for ctrl_id in ctrl_ids:
        dd_region, dd_base_year = 'dd-region{}'.format(ctrl_id), 'dd-base-year{}'.format(ctrl_id)
        app.callback(Output(dd_base_year, 'options'),
                     [Input(dd_region, 'value')])(load_base_year)

    for param in ["region", "base-year"]:
        # Toggle lock state.
        app.callback(Output('btn-dd-{}{}'.format(param, 0), "children"),
                     [Input('btn-dd-{}{}'.format(param, 0), "n_clicks")])(
            lambda n_clicks: "LOCK" if (n_clicks is None or n_clicks % 2 == 0) else "UNLOCK")
        # Disable drop down if parameter is locked.
        app.callback([Output('dd-{}{}'.format(param, ctrl_id), "disabled") for ctrl_id in ctrl_ids[1:]],
                     [Input('btn-dd-{}{}'.format(param, 0), 'children')])(lambda x: [x == "UNLOCK"] * len(ctrl_ids[1:]))
        # Fix value of slaves to master value when locked.
        app.callback([Output('dd-{}{}'.format(param, ctrl_id), "value") for ctrl_id in ctrl_ids[1:]],
                     [Input('dd-{}{}'.format(param, 0), 'value'),
                      Input('btn-dd-{}{}'.format(param, 0), 'children')],
                     [State('dd-{}{}'.format(param, ctrl_id), "value") for ctrl_id in ctrl_ids[1:]]) \
            (lambda master_value, x, *values: [master_value] * len(values) if x == "UNLOCK" else values)


# endregion

app = dash.Dash()
app.layout = layout()
register(app)

if __name__ == '__main__':
    app.run_server(**dict(debug=True, host="localhost", port=8002, threaded=True))

The GUI shows of a number of “cases”, each represented by two drop down selectors chained in a drill-down control flow. That is, the selection in the first drop down (a “region”) triggers an update of the options (the available “years”) for the second drop down. Additionally, the first case has a lock button for each option, which locks the corresponding option of all other cases.

To reproduce the issue,

  • Select “DK1” from the region selector of case 0
  • Click the lock button just below (the first one)

The lock action triggers the region selection for both case 1 and 2 correctly (you should see a disabled drop down with “DK1” selected in both cases). However, using Dash 1.8.0, the next step (the update of the year options) is only carried out correctly for case 2. That is, for case 1, the year drop down is still empty,

What makes this really weird is that the callback is actually executed (as seen in to the right in the google chrome network monitor). The reponse (not shown in the image) is also correct; in fact it is the same for both case 1 and case 2,

{“response”: {“props”: {“options”: [{“label”: 2018, “value”: 2018}, {“label”: 2019, “value”: 2019}]}}}

Hence it seems like the issue is clientside. When i downgrade to Dash 1.6.1, both case 1 and 2 are updated correctly .

Hi Emil,

I link the description of the bug I have seen with version 1.7.0 : Improper behavior with n_clicks in dash 1.7 with the source code to reproduce it. It looks really similar to yours, so I hope it might help to gather all weird behaviors in one single post :slight_smile:

1 Like