Skip to content

[BUG] dcc.Loading spinner does not trigger when callback Output uses ALL wildcard #3619

@i-murray

Description

@i-murray

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions