Are callback calls combined?

Hi,

I am developing a multi-tab app where tabs are loaded dynamically, and thus components are added and removed dynamically as well. That’s why I created callbacks which solely work with global Store inputs and outputs.

Broad structure is as follows:

  • Callback A collects load data from different sources and outputs to a data-field (Store) a load package with specific information. Here it’s just a button.
  • Callback B receives the output of A and starts the loading task which can take time. Once finished it outputs to a data-field (Store a load-state message
  • callback C receives the output of A and B (both Store types) with the intention that it receives the output of A first, prints the message, and then receives the output of B and prints another message

Intention: Print wait message after callbackA has finished and then print Success message after callback B has finished.
Observation: Callback C is not triggered after callback A. It is only triggered once A and B are finished and callback C has both outputs from A and B in its callback context.
Expectation: Callback C is triggered twice. 1. after callback A, 2. after callback B

As a test I added another callback D that only receives output from A and this one is triggered immediately.

Tested with dash 1.4 and dash 2.9, fresh install

Is this intended behaviour? Is there a way to avoid combining callbacks?

Thanks for your help!
Matt

See below code as an example without tabs but same behaviour that I experience.

import time
from dash import html, dcc,  Input, Output, callback, Dash, callback_context, no_update

app = Dash(__name__, suppress_callback_exceptions=False)
server = app.server

app.layout = html.Div(id="layoutroot", children=[

    html.Button("Load", id="my-button"),
    html.Div(id="waiting-element-cbC"),
    html.Div(id="waiting-element-cbD"),
	dcc.Store(id="load-data"),
	dcc.Store(id="after-load"),
])

@app.callback(
    Output("load-data", "data"),
    Input("my-button", "n_clicks")
)
def cbA(i):
    """
    Collects load data from different sources and prepares a load package
    """
    propid = None
    if callback_context.triggered:
        propid = callback_context.triggered[0]['prop_id'].split('.')[0]

    print(f"buttonclick propid {propid}")    
    if propid == "my-button":
        return "PREPARE DATA"
    return no_update

@app.callback(
    Output("after-load", "data"),
    Input("load-data", "data"),
)
def cbB(i):
    """
    Callback without any DOM elements attached. So it will work globally on a multi-tab environment.
    Receives load package and starts long loading task.
    """
    propid = None
    if callback_context.triggered:
        propid = callback_context.triggered[0]['prop_id'].split('.')[0]

    print(f"longtask propid {propid} .. params: {i}")

    # Long running loading task
    time.sleep(2)

    if propid == "load-data":
        return "DATA LOADED"
    return no_update

@app.callback(
    Output("waiting-element-cbC", "children"),
    Input("load-data", "data"),
    Input("after-load", "data"),
)
def cbC(loaddata, afterload):
    """
    Shows loading status with two inputs 
    """
    propids = []
    if callback_context.triggered:
        propids = [i['prop_id'].split('.')[0] for i in callback_context.triggered]

    print(f"CB3 propid {propids} .. params: {loaddata}, {afterload}")

    if "load-data" in propids:
        return f"Loading"

    if "after-load" in propids:
        return "Finished"

    return no_update

@app.callback(
    Output("waiting-element-cbD", "children"),
    Input("load-data", "data"),
)
def cbD(loaddata):
    """
    Shows loading status with only load-data input
    """
    propids = []
    if callback_context.triggered:
        propids = [i['prop_id'].split('.')[0] for i in callback_context.triggered]

    print(f"CB4 propid {propids} .. params: {loaddata}")

    if "load-data" in propids:
        return f"Loading"

    return no_update


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