How do I make dash wait before triggering a callback?

This is a continuation of my previous post, where I’m using this code to make the responsive navbar feature

I’m getting this error when uploading the layout with a grid (id’s: daggrid or daggrid2)

A nonexistent object was used in an Input of a Dash callback.

This problem started happening after making this navbar, because now the dropdowns are always active, but they are triggering a callback whose input is not yet loaded, just like explained here, but with the active tab instead.

However, I can’t simply remove the dropdowns as input like in that example, because they are necessary to update the graph’s content, I don’t want to make they States and use a button to trigger the callback either, because I don’t the user to be moving the cursor every time and the n_submit prop which triggers a callback when the user presses ‘Enter’ is only available for dcc.Input afaik.

Therefore, the only way to make it work I can think is to make dash wait a few ms before triggering the callback when I change the page, then it works normally.

Here is the code for the MRE:

Summary

app.py

import dash
from dash import Dash, html, dcc, callback, Input, Output, State
import dash_ag_grid as dag

import dash_mantine_components as dmc


import plotly.express as px
import pandas as pd

app = Dash(__name__, use_pages=True, suppress_callback_exceptions=True)
server = app.server




df = px.data.tips()
df = df.to_dict('records')


iterator = 0

page = list(dash.page_registry.values())


menu1 = dmc.Menu(
    [
        dmc.MenuTarget(dmc.Button('Pages')),
        dmc.MenuDropdown(
            [
                dmc.MenuItem(
                    'Page 1',
                    href=page[0]['path'],
                ),

                dmc.MenuItem(
                    'Page 2',
                    href=page[1]['path'],
                ),
            ]
        )
    ]
)



navwidth = 1


#----------------------------------------------------------------------------------------------------
app.layout = \
    html.Div(
        children=[
            dmc.Navbar(
                 id='sidebar',
                 fixed=False,
                 hidden=True,
                 width={"base": navwidth},
                 position='right',
                 children=[],
                 style={
                     "overflow": "hidden",
                     "transition": "width 0.3s ease-in-out",
                     "background-color": "#f4f6f9",
                 },
            ),

            html.Div(
                children=[
                    dmc.Grid(
                        children=[
                            dmc.Col(
                                dmc.Burger(id='sidebar-button'),
                                span='content',
                            ),

                            dmc.Col(
                                html.Div(
                                    children=[
                                        "Minimal reproducible example"
                                    ],
                                    style={'fontSize': 30, 'textAlign': 'left'}),
                                span='content', offset=2),

                            dmc.Col(menu1, span='content', offset=0),
                        ]),

                    html.Hr(),

                    html.Div(
                        children=[
                            dash.page_container,
                        ],
                    )
                ],
                style={"display": "flex", "maxWidth": "100vw", "overflow": "hidden",
                       "flexGrow": "1", "maxHeight": "100%", "flexDirection": "column"},
                id="content-container"
            ),




            #Store and Location components
            dcc.Location(id='url', refresh=True),
            dcc.Store(id='data-store', data=df),
            dcc.Store(id='page-changes-store', data=iterator),
        ],
        style={"display": "flex", "maxWidth": "100vw", "overflow": "hidden", "maxHeight": "100vh",
               "position": "absolute", "top": 0, "left": 0, "width": "100vw"},
        id="overall-container"
    )




#--------------------------------------------------------------------
@callback(
    Output("sidebar", "width"),
    Input("sidebar-button", "opened"),
    State("sidebar", "width"),
    prevent_initial_call=True,
)
def drawer_demo(opened, width):
    if width["base"] == navwidth:
        return {"base": 200}
    else:
        return {"base": navwidth}








def pg1():
    drop1 = dmc.Select(id='drop1', data=['sex', 'smoker'], value='sex')
    return drop1

def pg2():
    drop2 = dmc.Select(id='drop2', data=['sex', 'smoker'], value='sex')
    return drop2

@callback(
    Output('sidebar', 'children'),
    Output('page-changes-store', 'data'),
    Input('url', 'pathname'),
    State('page-changes-store', 'data'),
)

def nav_content(url, iterator):

    print('\nNum of times the page was changed:', iterator)
    iterator += 1

    nav_content = {
        '/': pg1(),
        '/pg2': pg2(),
    }.get(url)

    return nav_content, iterator





if __name__ == "__main__":
    app.run(debug=True, port=8060)

pg1.py

import dash
from dash import Dash, html, dcc, Input, Output, State, callback, ctx
import dash_ag_grid as dag

from dash.exceptions import PreventUpdate
import dash_mantine_components as dmc


import plotly.express as px
import pandas as pd

from datetime import datetime


external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

dash.register_page(__name__, name='Page 1', path='/')




layout = \
    html.Div([
        html.Div(id='grid-div', children=[]),


        html.Div(id='plot-div'),
    ])




#-------------------------------------------------------------------------------------------------
@callback(
    Output('grid-div', 'children'),
    Input('data-store', 'data'),
)

def make_grid(data):
    df = pd.DataFrame(data)

    grid = dag.AgGrid(
        id='daggrid',
        rowData=data,
        columnDefs=[{'field': i} for i in df.columns]
    )

    return grid





@callback(
    Output('plot-div', 'children'),
    Input('daggrid', 'virtualRowData'),
    Input('drop1', 'value'),
    prevent_initial_call=True
)

def filter_data(rows, drop):

    now = datetime.now()
    current_time = now.strftime('%T.%f')[:-3]

    comp_id = ctx.triggered_id if not None else 'nothing'

    print('\nPage 1 callback was triggered by:', comp_id)

    if not rows:
        print('Page 1 rows is 0 | Time:', current_time)
        raise PreventUpdate

    print('Page 1 rows is not 0 | Time:', current_time)
    print('Page 1 rows is None:', rows is None)

    dff = pd.DataFrame(rows)

    fig = px.scatter(dff, x='total_bill', y='tip', color=drop)

    return dcc.Graph(figure=fig)



if __name__ == '__main__':
    app.run(debug=True)

pg2.py

import dash
from dash import Dash, html, dcc, Input, Output, State, callback, ctx
import dash_ag_grid as dag

from dash.exceptions import PreventUpdate
import dash_mantine_components as dmc


import plotly.express as px
import pandas as pd

from datetime import datetime



external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

dash.register_page(__name__, name='Page 2', path='/pg2')



layout = \
    html.Div([
        html.Div(id='grid-div2', children=[]),


        html.Div(id='plot-div2'),
    ])


#-------------------------------------------------------------------------------------------------
@callback(
    Output('grid-div2', 'children'),
    Input('data-store', 'data'),
)
def make_grid(data):
    df = pd.DataFrame(data)

    grid = dag.AgGrid(
        id='daggrid2',
        rowData=data,
        columnDefs=[{'field': i} for i in df.columns]
    )

    return grid


@callback(
    Output('plot-div2', 'children'),
    Input('daggrid2', 'virtualRowData'),
    Input('drop2', 'value'),
)


def filter_data(rows, drop):

    now = datetime.now()
    current_time = now.strftime('%T.%f')[:-3]

    comp_id = ctx.triggered_id if not None else 'nothing'

    print('\nPage 1 callback was triggered by:', comp_id)

    if not rows:
        print('Page 2 rows is 0 | Time:', current_time)
        raise PreventUpdate

    print('Page 2 rows is not 0 | Time:', current_time)
    print('Page 2 rows is None:', rows is None)

    dff = pd.DataFrame(rows)

    fig = px.scatter(dff, x='total_bill', y='tip', color=drop)

    return dcc.Graph(figure=fig)



if __name__ == '__main__':
    app.run(debug=True)

Important observations:

  1. I already used suppress_callback_exceptions=True as an option in the main app;
  2. I’m also using dcc.Store to share data between pages, otherwise this problem doesn’t happen because the grid would load before the callback. But I want to reuse the grid instead of creating it on each page, it’s also useful for caching;
  3. Page 1 uses prevent_initial_call=True while page 2 doesn’t, this makes the error message as soon the app starts if I start it on page 2 but not page 1;
  4. Despite what I said, the ctx.triggered_id tells it’s the grids themselves that are triggering the callback, but it’s not them that are causing the problem, using time.sleep() inside the nav_content(url, iterator) function in the main app makes it clear that is the dropdown that triggers the callback;
  5. When the page is refreshed, the grid virtualRowData is initially 0, then it’s loaded, but using preventUpdate when it’s recognized as 0 is not enough to supress the error message, as per point 3.

Hi @Galliard

When you switch pages, anything that is used as an input in a callback will trigger the callback.

So the callback with the dropdown and the grid is triggered, but since a different callback adds the grid to the layout, the grid doesn’t exist yet.

You can fix this by defining the grid in the layout and updating the rowData and columnDefs in the callback (rather than returning the entire grid as children)