Circular Dependency message in browser console with Dash 1.11

In one of my applications, I have a feature that allows the use of preset values for different dropdown menus. The users can choose presets from a dropdown which set the values of other “children” dropdown menus to commonly used value combinations. The users still have the option to use custom values in the children dropdown menus, in which case the value of the preset dropdown menu is cleared to avoid the perception that a certain preset is used when the values of the children dropdowns have changed. Also, if the combination of values of the children dropdowns match a preset, the preset dropdown value is set to that preset.

The example app below is an isolated version of that feature:

import dash
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate

# Define presets with a name and values for the dropdowns A & B
presets = {"Preset 1 (A1-B3)": ["A1", "B3"], "Preset 2 (A3-B2)": ["A3", "B2"]}

app = dash.Dash(__name__)

app.layout = html.Div(
    [
        html.Label("Presets"),
        dcc.Dropdown(
            id="presets_dropdown",
            options=[dict(label=preset, value=preset) for preset in presets],
        ),
        html.Label("Dropdown A"),
        dcc.Dropdown(
            id="dropdown_a",
            options=[dict(label=option, value=option) for option in ["A1", "A2", "A3"]],
        ),
        html.Label("Dropdown B"),
        dcc.Dropdown(
            id="dropdown_b",
            options=[dict(label=option, value=option) for option in ["B1", "B2", "B3"]],
        ),
    ],
    style={"width": "200px"},
)


@app.callback(
    [Output("dropdown_a", "value"), Output("dropdown_b", "value")],
    [Input("presets_dropdown", "value")],
    [State("dropdown_a", "value"), State("dropdown_b", "value")],
)
def apply_preset(preset, a, b):
    # Apply the preset only if it differs from current values
    if preset is not None and presets[preset] != [a, b]:
        return presets[preset]
    # Otherwise do nothing (avoid circular reference)
    else:
        raise PreventUpdate


@app.callback(
    Output("presets_dropdown", "value"),
    [Input("dropdown_a", "value"), Input("dropdown_b", "value")],
    [State("presets_dropdown", "value")],
)
def clear_or_match_presets_dropdown(a, b, preset):
    if preset is not None:
        # If dropdowns match the current preset, do nothing
        if [a, b] == presets[preset]:
            raise PreventUpdate
        # else clear the preset by returning None (implicit)
    else:
        # If the dropdowns match a preset, set the presets dropdown to that preset
        for preset_name, values in presets.items():
            if [a, b] == values:
                return preset_name
        # If no match, do nothing (i.e. leave the preset to None)
        raise PreventUpdate


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

I use dash.exceptions.PreventUpdate in both callbacks to avoid an actual circular dependencies condition. Up until Dash version 1.10, this was working fine with no errors reported in the browser console. After upgrading to Dash 1.11, the feature still works as expected but I get the following error in the browser console:

{message: "Circular Dependencies", html: "Error: Dependency Cycle Found: dropdown_a.value -> presets_dropdown.value -> dropdown_a.value"}

I don’t know if I should report this as an issue? From the dependencies only, there is technically a condition of circular dependencies. However it is prevented from ever getting in an endless update loop by the use of PreventUpdate at strategic locations. The application and this feature still works correctly which is the most important but the console error that is new with Dash 1.11 is somewhat annoying. This is part of a relatively complex app and I like to keep the console free of errors.

Tested in Chrome Version 81.0.4044.122 on Windows 10 Professional as well as Firefox 75.0 on Ubuntu 18.04.

1 Like

Thanks for the note @drgfreeman! Funny, we did check for circular dependencies before v1.11 but there must have been a bug (perhaps related to the multi-output callback) that prevented us from detecting this loop before.

We’ve had a number of discussions internally about how to make circular dependencies work - see eg https://github.com/plotly/dash/issues/889 - but this hasn’t made it to the top of the priority list yet.

One thing I’ll note about v1.11 though - The refactoring involved in pattern-matching callbacks, which pre-calculates all callbacks in the chain and then dispatches them in sequence when they cease to be blocked by others in the chain, made it so that we will normally never enter an infinite loop - each callback will be executed at most once as a result of any given stimulus. That means if you were expecting to go around your loop twice or something before settling down and halting, that’s no longer going to happen.

That said there is a way around this: new callbacks can be added to the set by creating new components in a children callback. This is a kind of circularity we currently have no way of detecting or halting. So if you really want to avoid this error and are willing to muddy your app logic a little to do it, you could for example put presets_dropdown into a container div and return the entire dropdown as the children of that div rather than just setting its value prop. Is that worth it to avoid the error message? Your call, that’s the only thing I can think of before we implement a real way to specify circular callbacks.

1 Like

Thanks @alexcjohnson. I was wondering why this was now reported as an error in the browser while it wasn’t prior Dash version 1.11. My app works fine and I will not make it more complicated to avoid the error message that users don’t see.

Note that the changes to the callbacks handling in Dash 1.11 fixed two issues with tables and graph not updating that I never managed to solve. Those were user facing issues so overall it’s a win!

1 Like