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)