-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Description
Describe your context
dash 4.0.0
dash-bootstrap-components 2.0.4
dash-table 5.0.0
- OS: Windows 11
- MS Edge
- Version 145
Describe the bug
dcc.Loading never displays the loading spinner when a callback's Output uses the ALL wildcard in a pattern-matching ID, e.g.:
Output({"type": "my-graph", "id": ALL, "name": MATCH}, "figure")The same callback with MATCH or a fully-specified ID works correctly — the spinner appears during the callback execution.
Root cause
In dash-renderer/src/actions/callbacks.ts, loadingOutputs is constructed as:
loadingOutputs = outputs.map(function (out) {
return {
path: getPath(paths, out.id),
property: out.property?.split('@')[0],
id: stringifyId(out.id)
};
});When the output spec contains ALL, unwrapIfNotMulti returns the resolved outputs as an array (because isMultiValued returns true). This array is pushed into outputs as a single element, producing a nested structure like [[{id, property}, ...]]. When .map() iterates over this, out is the inner array, so out.id is undefined → getPath returns undefined → the loading reducer receives a bogus path → dcc.Loading never matches it → no spinner.
Notably, the same function already uses flatten(outputs) correctly on L829 and L912 when processing the actual response data — the flattening was simply missed for the loading dispatch.
Fix
Change outputs.map(…) to outputs.flat().map(…) on L794 so the nested arrays from multi-valued outputs are flattened before resolving paths:
loadingOutputs = outputs.flat().map(function (out) {
// ...same body
});Expected behavior
dcc.Loading should display the loading spinner for callbacks with ALL in the Output, the same as it does for MATCH or fully-specified outputs.
Minimal reproducible example
"""Minimal Dash app to test dcc.Loading with ALL vs specific callback outputs."""
import time
import dash_bootstrap_components as dbc
from dash import ALL, MATCH, Dash, Input, Output, dcc, html
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
FIGURE_A = {"data": [{"x": [1, 2, 3], "y": [1, 3, 2]}], "layout": {"title": "Option A"}}
FIGURE_B = {"data": [{"x": [1, 2, 3], "y": [3, 1, 2]}], "layout": {"title": "Option B"}}
RADIO_OPTIONS = [
{"label": "Option A", "value": "a"},
{"label": "Option B", "value": "b"},
]
graph_id_all = {"type": "graph-all", "id": "component", "name": "test"}
graph_id_match = {"type": "graph-match", "id": "component", "name": "test"}
app.layout = html.Div([
html.H3("dcc.Loading test: ALL vs MATCH in Output"),
dbc.Row([
# ALL output — spinner BROKEN
dbc.Col([
html.H5("ALL in Output (broken)"),
dbc.RadioItems(options=RADIO_OPTIONS, value="a",
id={"type": "radio-all", "id": "component", "name": "test"}, inline=True),
dcc.Loading(
dcc.Graph(id=graph_id_all, figure=FIGURE_A),
type="dot",
overlay_style={"visibility": "visible", "opacity": 0.5, "filter": "blur(2px)"},
),
], width=6),
# MATCH output — spinner WORKS
dbc.Col([
html.H5("MATCH in Output (works)"),
dbc.RadioItems(options=RADIO_OPTIONS, value="a",
id={"type": "radio-match", "id": "component", "name": "test"}, inline=True),
dcc.Loading(
dcc.Graph(id=graph_id_match, figure=FIGURE_A),
type="dot",
overlay_style={"visibility": "visible", "opacity": 0.5, "filter": "blur(2px)"},
),
], width=6),
]),
], className="p-3")
@app.callback(
Output({"type": "graph-all", "id": ALL, "name": MATCH}, "figure"),
Input({"type": "radio-all", "id": ALL, "name": MATCH}, "value"),
prevent_initial_call=True,
)
def update_all(values):
time.sleep(2)
return [FIGURE_A if values[0] == "a" else FIGURE_B]
@app.callback(
Output({"type": "graph-match", "id": MATCH, "name": MATCH}, "figure"),
Input({"type": "radio-match", "id": MATCH, "name": MATCH}, "value"),
prevent_initial_call=True,
)
def update_match(value):
time.sleep(2)
return FIGURE_A if value == "a" else FIGURE_B
if __name__ == "__main__":
app.run(debug=True, port=8051)