Circular Dependency Problem; Disabling/enabling Button

Dear Dash Community,

I have a problem with a circular dependency in a multi-page Dash app. Usually, I ignore those warnings as the callbacks still fire, but this time it’s different. See below a simplified version:

import dash
import time
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output
from dash import callback_context, no_update

app = dash.Dash(__name__,  external_stylesheets=[dbc.themes.BOOTSTRAP])

app.layout = dbc.Card(
        [
            html.Div(id="start-trigger"),
            html.Div(id="finished-trigger"),
            dbc.Button(
                dbc.Row(
                    [
                        dbc.Spinner(id="button-spinner", size="sm",
                                    spinner_style={"display": "none"}),
                        "Start Calculation"
                    ],
                    no_gutters=True
                ),
                id="start-button",
                color="success",
                size="md",
                n_clicks=0
            ),
        ]
    )


@app.callback(
    [Output("start-button", "disabled"),
     Output("button-spinner", "spinner_style"),
     Output("start-trigger", "children")],
    [Input("start-button", "n_clicks"),
     Input("finished-trigger", "children")],
    prevent_initial_call=True)
def helper_calculation(_, __):
    if callback_context.triggered[0]["prop_id"] == "start-button.n_clicks":
        return True, {"margin": ".3rem .65rem 0 0"}, "start-trigger"
    else:
        return False, {"display": "none"}, no_update


@app.callback(
    Output("finished-trigger", "children"),
    [Input("start-trigger", "children")],
    prevent_initial_call=True)
def long_calculation(_):
    time.sleep(5)
    return "calculation finished"


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

The code is supposed to do the following:

  1. Upon button press, the button shall be deactivated to prevent further callbacks of the same calculation, the progress-spinner shall be shown, and the start-trigger (invisible Div) shall be updated

  2. Upon a change of the start-trigger, the long calculation shall be started and once it’s finished, the finished-trigger shall be updated

  3. Upon a change of the finished-trigger, the button shall be activated again and the progress-spinner shall become invisible again. Notice the “no_update” in the else tree of the first callback to prevent the same calculation from happening again.

Currently, step 1 and 2 work. But although the finished trigger is updated, the first callback is not triggered and hence the spinner keeps spinning and the button stays deactivated

Any idea how I can make this work?

I was able to solve my problem by replacing the whole button incl. spinner. This way, Dash seems not to recognize the circular dependency and fires the callback. See the code below:

import dash
import time
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output
from dash import callback_context, no_update
from dash.exceptions import PreventUpdate

app = dash.Dash(__name__,  external_stylesheets=[dbc.themes.BOOTSTRAP])

app.layout = dbc.Card(
        [
            html.Div(id="start-trigger"),
            html.Div(
                dbc.Button(
                    dbc.Row(
                        [
                            dbc.Spinner(id="button-spinner", size="sm",
                                        spinner_style={"display": "none"}),
                            "Start Calculation"
                        ],
                        no_gutters=True
                    ),
                    id="start-button",
                    color="success",
                    size="md",
                    n_clicks=0
                ),
                id="button-container"
            )
        ]
    )


@app.callback(
    [Output("start-button", "disabled"),
     Output("button-spinner", "spinner_style"),
     Output("start-trigger", "children")],
    [Input("start-button", "n_clicks")],
    prevent_initial_call=True)
def helper_calculation(_):
    return True, {"margin": ".3rem .65rem 0 0"}, "start-trigger"


@app.callback(
    Output("button-container", "children"),
    [Input("start-trigger", "children")],
    prevent_initial_call=True)
def long_calculation(_):
    time.sleep(5)
    return "calculation finished", dbc.Button(
        dbc.Row(
            [
                dbc.Spinner(id="button-spinner", size="sm",
                            spinner_style={"display": "none"}),
                "Start Calculation"
            ],
            no_gutters=True
        ),
        id="start-button",
        color="success",
        size="md",
        n_clicks=0
    )


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