Dynamic IDs made via set_progress not returned via ctx.triggered_id

I want to create a series of cancel buttons, each with their own unique IDs. These buttons are being made via set_progress, because there’s other steps that would occur afterwards. The buttons would then be able to stop these other steps, if need be.

I’ve come across an issue where I can inspect the frontend, and see these dynamic button IDs there, but the IDs are not returned via the terminal (rather ctx.triggered_id returns None).

I’ve made a minimum example, below. Please let me know if you think this issue is:
a) Something I’m doing wrong
b) A bug
c) A limitation, with a low likelihood of being implemented
d) Other!

You can reproduce the error by clicking on one of the STOP buttons before the “Finished…” text appears beside the button. You will see: The id was: None and n_clicks was: [] in the terminal. When the callback to create the button is finished, you can see that Callback #2 retriggers, with The id was: {'index': 1, 'type': 'cancel-button'} and n_clicks was: [None] which leads me to believe that the set_progress isn’t actually creating the dynamic ID completely.

import dash
from dash.dependencies import Output, Input, State, ALL
from dash import dcc, html, dash_table, DiskcacheManager, CeleryManager, ctx, no_update
import dash_bootstrap_components as dbc
import time

# Create background callback manager
import diskcache
cache = diskcache.Cache("./cache")
background_callback_manager = DiskcacheManager(cache)

# Create app
app = dash.Dash(__name__, background_callback_manager=background_callback_manager)

# Create layout
app.layout = html.Div(
    [
        dbc.Button(id="start-button", children="START"),
        html.Div(id="display-field-area"),
        dcc.Store(id="cancel-store", storage_type="local", data=None),
    ]
)

# Callback #1: Updates the frontend
@app.callback(
    Output("display-field-area", "children"),
    Input('start-button', 'n_clicks'),
    State("display-field-area", "children"),
    background=True,
    progress=[Output("display-field-area", "children")],
    cancel=[Input("cancel-store", "data")], # This is a store because there are multiple unique buttons which can cause cancellation! See Callback #2 below!
    prevent_initial_call=True
)
def update_progress(set_progress, n_clicks, children):
    if children is None:
        children = []
    if n_clicks > 0:
        new_button = dbc.Button(
            id={"type": "cancel-button", "index": n_clicks},
            children=f"STOP {n_clicks}"
        )
        children.append(new_button)
        set_progress([children])
        
        time.sleep(5)
        children.append(f"Finished adding button {n_clicks}")
    return children

# Callback #2: Updates the cancellation store
@app.callback(
    Output("cancel-store", "data"),
    Output({"type": "cancel-button", "index": ALL}, "n_clicks"),
    Input({"type": "cancel-button", "index": ALL}, "n_clicks"),
    prevent_initial_call=True
)
def trigger_store_udpate(n_clicks):
    button_id = ctx.triggered_id
    print("The id was:", button_id, "and n_clicks was:", n_clicks)
    for clicks in n_clicks:
        if clicks is not None and clicks > 0:
            return id, [0] * len(n_clicks)
    return no_update, [0] * len(n_clicks)


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

@adamschroeder - any ideas?

What is id?

I believe you’re returning the id() built-in function in Python instead of button_id.

1 Like

Good pickup @TARpa! Unfortunately, I can’t seem to edit it above, but here’s the full code updated. The issue is that even at the print() statement, the button_id is returning as None.

import dash
from dash.dependencies import Output, Input, State, ALL
from dash import dcc, html, dash_table, DiskcacheManager, CeleryManager, ctx, no_update
import dash_bootstrap_components as dbc
import time

# Create background callback manager
import diskcache
cache = diskcache.Cache("./cache")
background_callback_manager = DiskcacheManager(cache)

# Create app
app = dash.Dash(__name__, background_callback_manager=background_callback_manager)

# Create layout
app.layout = html.Div(
    [
        dbc.Button(id="start-button", children="START"),
        html.Div(id="display-field-area"),
        dcc.Store(id="cancel-store", storage_type="local", data=None),
    ]
)

# Callback #1: Updates the frontend
@app.callback(
    Output("display-field-area", "children"),
    Input('start-button', 'n_clicks'),
    State("display-field-area", "children"),
    background=True,
    progress=[Output("display-field-area", "children")],
    cancel=[Input("cancel-store", "data")], # This is a store because there are multiple unique buttons which can cause cancellation! See Callback #2 below!
    prevent_initial_call=True
)
def update_progress(set_progress, n_clicks, children):
    if children is None:
        children = []
    if n_clicks > 0:
        new_button = dbc.Button(
            id={"type": "cancel-button", "index": n_clicks},
            children=f"STOP {n_clicks}"
        )
        children.append(new_button)
        set_progress([children])
        
        time.sleep(5)
        children.append(f"Finished adding button {n_clicks}")
    return children

# Callback #2: Updates the cancellation store
@app.callback(
    Output("cancel-store", "data"),
    Output({"type": "cancel-button", "index": ALL}, "n_clicks"),
    Input({"type": "cancel-button", "index": ALL}, "n_clicks"),
    prevent_initial_call=True
)
def trigger_store_udpate(n_clicks):
    button_id = ctx.triggered_id
    print("The id was:", button_id, "and n_clicks was:", n_clicks)
    for clicks in n_clicks:
        if clicks is not None and clicks > 0:
            return button_id, [0] * len(n_clicks)
    return no_update, [0] * len(n_clicks)


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

@alexcjohnson - any ideas? Is this related to https://github.com/plotly/dash/pull/1702#pullrequestreview-733024977

Can you clarify your issue?

This is what I see in the terminal when I run your app:

* Serving Flask app 'dash_app'
 * Debug mode: on
running
The id was: {'index': 1, 'type': 'cancel-button'} and n_clicks was: [None]
The id was: {'index': 1, 'type': 'cancel-button'} and n_clicks was: [0, None]
The id was: {'index': 1, 'type': 'cancel-button'} and n_clicks was: [0, 0, None]
The id was: {'index': 1, 'type': 'cancel-button'} and n_clicks was: [1, 0, 0]
The id was: {'index': 2, 'type': 'cancel-button'} and n_clicks was: [0, 1, 0]
The id was: {'index': 3, 'type': 'cancel-button'} and n_clicks was: [0, 0, 1]