Chained callback is called in unexpected way

Problem

I figured out occasionally that if a callback returns a dynamic component that consists of subcomponents it can call another callback those subcomponents assigned to as Inputs.

Brief example

@callback(
Output('card-container', 'children'),
Input('button', n_clicks))
def add_card(n_clicks):
return  dbc.Card(id=card_id,
                 children=[
                        dbc.Button(html.I(className="bi bi-pencil-square"),
                             id={'type': IdTypeEnum.BUTTON_OPEN.value, 'index': card_id})
                        dbc.Button(html.I(className="bi bi-archive"),
                                id={'type': IdTypeEnum.BUTTON_ARCHIVE.value, 'index': card_id}) 
                 ]),


@callback(
Output('cytoscape', 'elements'),
Input('toggle', 'n_cliks')
Input({'type': IdTypeEnum.BUTTON_ARCHIVE.value, 'index': ALL}, 'n_clicks'),
Input({'type': IdTypeEnum.BUTTON_OPEN.value, 'index': ALL}, 'n_clicks'))
def handle_elements(n_clicks1, n_clicks2, n_clicks3)
     print(ctx.triggered_id)

Due to the documentation i expect handle_elements stay untouched as i didn’t declare Buttons as Output of the add_card callback

Questions

  1. Has handle_elements to be called by add_card? is such the mechanic correct?
  2. If it’s correct, can it be disabled or avoided somehow? i’d like to have the inputs triggered by a real user only, not by a callback chain.

Hello @kvdm.dev,

Yes, this is an issue with adding dynamic components, especially with pattern-matching ALL.

Check out what I did here:

@app.callback(
    Output("editorMenu", "is_open"),
    Output("editor", "loadFigure"),
    Output("chartId", "value"),
    Output("oldSum", "data"),
    Input({"type": "dynamic-edit", "index": ALL}, "n_clicks"),
    State({"type": "dynamic-output", "index": ALL}, "figure"),
    State("oldSum", "data"),
    State({"type": "dynamic-card", "index": ALL}, "id"),
    prevent_initial_call=True,
)
def edit_card(edit, figs, oldSum, ids):
    if sum(edit) < oldSum:
        oldSum = sum(edit)
    if sum(edit) > 0 and sum(edit) > oldSum:
        oldSum = sum(edit)
        if ctx.triggered[0]["value"] > 0:
            for i in range(len(ids)):
                if ids[i]["index"] == ctx.triggered_id["index"]:
                    if figs[i]["data"]:
                        return True, figs[i], ctx.triggered_id["index"], oldSum
                    return (
                        True,
                        {"data": [], "layout": {}},
                        ctx.triggered_id["index"],
                        oldSum,
                    )
    return no_update, no_update, no_update, oldSum

I have a dcc.Store that stores the combined total of all the n_clicks, if the total doesnt increase, then is returns no_update.

1 Like

I’ve solved it similarly, but have a server side storage istead of dcc.Store for counters, flags and etc.

I would rather like to understand here should i do this workaround always? As i see, you do it the same way and it means yes, i should. Isn’t it?

Yes, for things that get added in this way, it is necessary to use this workaround. It seems to be the only useful way to handle this, for the time being.

2 Likes