dash-core-components icon indicating copy to clipboard operation
dash-core-components copied to clipboard

Loading component nesting causing unwanted behaviour

Open mikesmith1611 opened this issue 6 years ago • 3 comments

I notice a "bug" in the Loading component when trying to acheice the following:

  • A global wrapper around the "page-body" element that shows when loading a page (using dcc.Location)

  • Within the body of this page there are also some elements that I would like to have a separate Loading component on callback and some which I do not want to have a Loading component.

  • When the dcc.Link is clicked, the higher level Loading component is seen.

  • Once the "page-body" is loaded i click on a button that updates a component that is not within the lower level Loading component. The higher level loading component notices a change in it's children and renders the Loading animation. This is not desirable in this case but makes sense. On some clicks of the button there is a double triggering of the Loading animation presumably due to multiple children being updated.

  • When I click the button which triggers a callback to a component within the lower level Loading component the higher level Loading component is not triggered

loading-bug

# -*- coding: utf-8 -*-
import dash
import dash_html_components as html
import dash_core_components as dcc
import plotly.graph_objs as go
import time
import numpy as np

from dash.dependencies import Input, Output, State

app = dash.Dash(__name__)

app.scripts.config.serve_locally = True
app.config['suppress_callback_exceptions']=True
app.layout = html.Div([
    dcc.Location(id='url', pathname='/'),
    dcc.Loading(html.Div(id='page-body'))
])

def make_layout(pathname):
    if pathname == '/':
        return html.Div([
            dcc.Link('Navigate to slow loading page', href='/mypage')
        ])

    elif pathname == '/mypage':
        time.sleep(3)

        return html.Div([
                html.Div([dcc.Graph(id='figure-1'), html.Div(id='figure-info')]),
                html.Button(id="input-1", children='Input triggers parent spinner'),
                dcc.Loading(
                id="loading-2",
                children=[html.Div([dcc.Graph(id='figure-2')])],
                type="circle"),
                html.Button(id="input-2", children='Input triggers nested spinner')
            ], style={'width': 400})


@app.callback(Output('page-body', "children"), [Input("url", "pathname")])
def input_triggers_spinner(pathname):
    
    return make_layout(pathname)


@app.callback(Output("figure-1", "figure"), [Input("input-1", "n_clicks")])
def input_triggers_nested(n_clicks):
    time.sleep(2)
    fig = go.Scatter(
        x=np.random.random(size=10),
        y=np.random.random(size=10)
    )
    return dict(data=[fig])


@app.callback(Output("figure-info", "children"), [Input("figure-1", "figure")])
def input_triggers_nested(n_clicks):

    return html.Div(str(n_clicks))

@app.callback(Output("figure-2", "figure"), [Input("input-2", "n_clicks")])
def input_triggers_nested(n_clicks):
    time.sleep(2)
    fig = go.Scatter(
        x=np.random.random(size=10),
        y=np.random.random(size=10)
    )
    return dict(data=[fig])





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

Perhaps a way of getting round this is to supply a "child-level" argument to the loading componet so that it only monitors children rather than grandchildren etc. if this is even possible?

mikesmith1611 avatar Mar 12 '19 13:03 mikesmith1611

Hi, thanks for making an issue! A few comments:

  • Once the "page-body" is loaded i click on a button that updates a component that is not within the lower level Loading component. The higher level loading component notices a change in it's children and renders the Loading animation

I think that is expected behaviour. The Loading component will look at its children and their loading states, up till the point where there is another Loading component. It makes sense then that a child that's not inside a Loading component (the lower level Loading component in this case) does not get picked up by that Loading component. It's really just a wrapper that looks at its children.

  • When I click the button which triggers a callback to a component within the lower level Loading component the higher level Loading component is not triggered

Here too I think that's what's expected – because the component is wrapped in the lower level Loading component, that's what it will trigger. The fix here could be to simply take that component out of that particular Loading wrapper and put it somewhere where it is a child of the Loading wrapper you want.

valentijnnieman avatar Mar 20 '19 15:03 valentijnnieman

@valentijnnieman I believe the usage @mikesmith1611 is demonstrating is quite common. For example when loading multipage apps, one would like to only show the spinner when the URL changes, even if all content is still a child of the Loading component. That way the previous content is occluded by the spinner.

What would be nice is to be able to remove the behavior of refreshing the loader for every child updating or may be also specifying a maximum depth to traverse for checking updates.

astenuz avatar Nov 01 '20 03:11 astenuz

I agree with @astenuz and @mikesmith1611, would be great if we could specify the maximum depth for the Loading component.

tobiasbrodd avatar Sep 24 '21 12:09 tobiasbrodd