Open
Description
Describe the bug
"Click" button is the Input for two callbacks. When we click it, both should be triggered:
- the first one takes a long time (e.g. 3 seconds), and when it ends, it should update the "stop" button, which triggers the second callback too.
- the second callback returns quickly, it should return a "Progress" notification with the first trigger (when the "click" button triggers it) and a "Complete" notification when the "Stop" button triggers it - after the first callbacks finishes running.
- So these are chained callbacks apart from the "Click" button being an Input for both.
- Bug: we only see the "Progress" notification and after 3 seconds, when the first callbacks finishes running. The second callback is never triggered by the "Stop" button update as a result of the chained callback (we can see this by checking the triggered ids).
Screen.Recording.2024-02-21.at.17.28.05.mov
The developer who reported this experienced after upgrading from dash==2.6.2 to dash==2.15.0.
Hypothesis: this might be due to the 2.9.0 changes to accommodate duplicate outputs.
Code to reproduce the issue:
from dash import Output, Input, html, dcc, ctx, callback
from dash_iconify import DashIconify
import dash_mantine_components as dmc
import time
import dash
import random
app = dash.Dash(__name__)
app.layout =dmc.NotificationsProvider( html.Div([
html.Button("click", id="btn-start"),
html.Button("stop", id="btn-stop"), #style={'display': 'none'}),
html.Div(id="notify-container"),
]
))
@callback(
Output("btn-stop", "n_clicks"),
Input("btn-start", "n_clicks"),
)
def make_api_call(nc1):
# print("CALLBACK 1")
changed_id = [p['prop_id'] for p in ctx.triggered][0]
if "btn-start" in changed_id:
# making api call
print("api call")
time.sleep(3)
return 1
else :
return dash.no_update
@callback(
Output("notify-container", "children"),
Input("btn-start", "n_clicks"),
Input("btn-stop", "n_clicks"),
prevent_initial_call=True,
)
def notify(nc1, nc2):
# print("CALLBACK 2")
button_id = [p['prop_id'] for p in dash.callback_context.triggered][0]
if "start" in button_id:
return dmc.Notification(
id="my-notification",
title="Process initiated",
message="The process has started.",
loading=True,
color="orange",
action="show",
autoClose=False,
disallowClose=True,
)
elif "stop" in button_id:
return dmc.Notification(
id="my-notification",
title="Data loaded",
message="The process has started.",
color="green",
#action="show",
action="update",
icon=DashIconify(icon="akar-icons:circle-check"),
)
else :
return dash.no_update
if __name__ == '__main__':
app.run(debug=True)
Expected behavior
"Click" button should trigger both callbacks at the same time, which would result in "Progress" notification showing immediately and lasting 3 seconds, and then "Complete" notification replacing it:
Screen.Recording.2024-02-21.at.17.22.04.mov
This behaviour can be reproduced with dash>=2.9.x if we use duplicate outputs, but for users this would mean rewriting several callbacks, as this would be a breaking change otherwise:
from dash import Output, Input, html, callback_context as ctx, callback
import dash_mantine_components as dmc
from dash_iconify import DashIconify
import time
import dash
import random
app = dash.Dash(__name__)
app.layout =dmc.NotificationsProvider( html.Div([
html.Button("click", id="btn-start"),
html.Button("stop", id="btn-stop"), #style={'display': 'none'}),
html.Div(id="notify-container"),
]
))
@callback(
Output("btn-stop", "n_clicks"),
Input("btn-start", "n_clicks"),
prevent_initial_call=True
)
def make_api_call(nc1):
print("CALLBACK 1")
changed_id = ctx.triggered_id
if "btn-start" in changed_id:
# making api call
print("api call")
time.sleep(3)
return 1
else :
return dash.no_update
@callback(
Output("notify-container", "children", allow_duplicate=True),
Input("btn-start", "n_clicks"),
prevent_initial_call=True,
)
def notify(nc1):
print("CALLBACK 2")
button_id = ctx.triggered_id
if "start" in button_id:
return dmc.Notification(
id="my-notification",
title="Process initiated",
message="The process has started.",
loading=True,
color="orange",
action="show",
autoClose=False,
disallowClose=True,
)
else :
return dash.no_update
@callback(
Output("notify-container", "children"),
Input("btn-stop", "n_clicks"),
prevent_initial_call=True,
)
def notify(nc1):
print("CALLBACK 3")
button_id = ctx.triggered_id
if "stop" in button_id:
return dmc.Notification(
id="my-notification",
title="Data loaded",
message="The process has started.",
color="green",
#action="show",
action="update",
icon=DashIconify(icon="akar-icons:circle-check"),
)
else :
return dash.no_update
if __name__ == '__main__':
app.run(debug=True)
Describe your context
- replace the result of
pip list | grep dash
below
dash==2.15.0 # 2.6.2
dash-ag-grid==1.2.1
dash-bootstrap-components==1.0.3
dash-colorscales==0.0.4
dash-core-components==2.0.0
dash-cron==0.0.1
dash-dangerously-set-inner-html==0.0.2
dash-daq==0.5.0
dash-design-kit==1.6.7
dash-draggable==0.1.2
dash-embedded==2.0.0
dash-enterprise-auth==0.0.5
dash-html-components==2.0.0
dash-iconify==0.1.2
dash-mantine-components==0.12.1
dash-notes==0.0.3
dash-snapshots==1.4.6
dash-split-pane==1.0.0
dash-table==5.0.0