Multipage, single file per page callbacks not triggering in the order I expect

I’ve got a small minimal example to share. Really could use some help on this because I don’t know how to structure my app to get around it. I’ve read everything I can on the forum but I just don’t understand the app, the server wrapping the app, and how I can just put my callbacks where I want them and still have them function.

My expectation:
On URL change:
app.layout renders and loads data into dcc.Store component id=‘f_app_loaded’

THEN

pages/login detects that ‘f_app_loaded’ changed and does its callback to populate the change

Here’s what happens:
app.layout renders and loads data into dcc.Store component id=‘f_app_loaded’
pages/login does nothing.

If I add a 2 second delay to the app callback, then the page callback detects the callback properly.

I don’t understand how to set up the app, do I really have to put ALL of my callbacks on one .py file? That sounds like a nightmare to manage.

This is main.py

import flask
import dash

from dash import Dash, html, dcc, callback, Output, Input
import dash_bootstrap_components as dbc

import os
import time

server = flask.Flask(__name__)

app = Dash(__name__,
    server=server
    , external_stylesheets=[
        dbc.themes.BOOTSTRAP
    ]
    ,assets_folder=os.getcwd() + '/assets'
    , use_pages=True
    , eager_loading=False
    )

app.layout = html.Div([
    # dcc.Location(refresh="callback-nav", id='app_url_location')
    dcc.Location(refresh='', id='app_url_location')
    , dcc.Store(id='f_app_loaded', storage_type='session')
    ,dash.page_container
])

@callback(
    Output("f_app_loaded", "data")
    ,Input("app_url_location", 'pathname')
    )
def app_level_checks(app_level_path):
    print("load some data to trigger the page level callback (it doesn't work)!")
    #why does WAITING 2 seconds work?!
    #time.sleep(2)
    return {}

if __name__ == '__main__':
    server.run(host='localhost', port=8082, debug=False)

This is pages/login.py

import dash
from dash import html,State,dcc,callback,Input, Output, ALL, ctx, Patch, MATCH
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc

dash.register_page(__name__, path='/')
pname = __name__.split(".")[-1]

def layout(**kwargs):
    print("launch layout for login page")
    ly= html.Div(
        [
            dcc.Location(id='page_url_location')
            ,html.Div("login content that should be overwritten by the callback that will never trigger")

        ]
    , id='page-content'
    , **kwargs
    )
    return ly


@callback(
    Output('page-content','children')
    ,Input("f_app_loaded", "data")
    #,Input("f_app_loaded", "modified_timestamp")
    #,Input("page-content","children")
    #,Input("page_url_location","path")
    #,prevent_initial_call=True
)
def page_layout_cb(data
                   #,mod_ts
                   #,children
                   #,url_p
                   ):
    trg_id = ctx.triggered_id
    if not trg_id:
        print("I didn't get the triggered ID I'm owed")
        raise PreventUpdate
    print(f"dash_login page page_layout_cb from {trg_id}")
    components = [
        html.H1("Welcome to a broken app!")
        ,html.P("Please log in to continue")
    ]

    return components

1 Like

EDIT: it worked like a couple times and then stopped working back to old behavior. I’m so confused.

It works in this arrangement, but I really don’t like being forced to put all my callbacks on the main,py and giving up on function defined layouts.

II hope there’s some solution to this.

main.py

import flask
import dash

from dash import Dash, html, dcc, callback, Output, Input, ctx
import dash_bootstrap_components as dbc

from dash.exceptions import PreventUpdate

import os
import time
import uuid

server = flask.Flask(__name__)

app = Dash(__name__,
    server=server
    , external_stylesheets=[
        dbc.themes.BOOTSTRAP
    ]
    ,assets_folder=os.getcwd() + '/assets'
    , use_pages=True
    , eager_loading=False
    )

app.layout = html.Div([
    # dcc.Location(refresh="callback-nav", id='app_url_location')
    dcc.Location(refresh='', id='app_url_location')
    , dcc.Store(id='f_app_loaded', storage_type='session')
    ,dash.page_container
])

@callback(
    Output("f_app_loaded", "data")
    ,Input("app_url_location", 'pathname')
    )
def app_level_checks(app_level_path):
    print("load some data to trigger the page level callback (it doesn't work)!")
    #why does WAITING 2 seconds work?!
    #time.sleep(.25)
    return {'a':uuid.uuid4().hex}

@callback(
    Output('page-content','children')
    #,Input("f_app_loaded", "data")
    ,Input("f_app_loaded", "modified_timestamp")
    #,Input("page-content","children")
    #,Input("page_url_location","path")
    ,prevent_initial_call=True
)
def page_layout_cb(data
                   #,mod_ts
                   #,children
                   #,url_p
                   ):
    trg_id = ctx.triggered_id
    if not trg_id:
        print("I didn't get the triggered ID I'm owed")
        raise PreventUpdate
    print(f"dash_login page page_layout_cb from {trg_id}")
    components = [
        html.H1("Welcome to a broken app!")
        ,html.P("Please log in to continue")
    ]

    return components

if __name__ == '__main__':
    server.run(host='localhost', port=8082, debug=False)

login.py

import dash
from dash import html,State,dcc,callback,Input, Output, ALL, ctx, Patch, MATCH
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc

dash.register_page(__name__, path='/')
pname = __name__.split(".")[-1]

# def layout(**kwargs):
#     print("launch layout for login page")
#     ly= html.Div(
#         [
#             dcc.Location(id='page_url_location')
#             ,html.Div("login content that should be overwritten by the callback that will never trigger")
#
#         ]
#     , id='page-content'
#     , **kwargs
#     )
#     return ly

layout =  html.Div(
        [
            dcc.Location(id='page_url_location')
            ,html.Div("login content that should be overwritten by the callback that will never trigger")

        ]
    , id='page-content'

    )



# @callback(
#     Output('page-content','children')
#     ,Input("f_app_loaded", "data")
#     #,Input("f_app_loaded", "modified_timestamp")
#     #,Input("page-content","children")
#     #,Input("page_url_location","path")
#     #,prevent_initial_call=True
# )
# def page_layout_cb(data
#                    #,mod_ts
#                    #,children
#                    #,url_p
#                    ):
#     trg_id = ctx.triggered_id
#     if not trg_id:
#         print("I didn't get the triggered ID I'm owed")
#         raise PreventUpdate
#     print(f"dash_login page page_layout_cb from {trg_id}")
#     components = [
#         html.H1("Welcome to a broken app!")
#         ,html.P("Please log in to continue")
#     ]
#
#     return components

Hi @rrzzxx

Have you tried using dash-auth?

@rrzzxx

Even if this isn’t for authentication, the pattern you are struggling with should work. It’s possible to trigger a callback when the store changes. You can also update the store when the url changes. It is possible to use functions for layouts and for callbacks to be in any file of a multi-page app.

Step 1 Single Page App

Let’s start with the simplest use-case. A single page app that updates the layout when the app is first loaded.

Try running this app without making any changes and note the structure. One callback updates the store and another is triggered when the data changes:


import flask
from dash import Dash, html, dcc, callback, Output, Input, State
import dash_bootstrap_components as dbc
import uuid

server = flask.Flask(__name__)

app = Dash(
    __name__,
    server=server,
    external_stylesheets=[dbc.themes.BOOTSTRAP],
)

app.layout = html.Div(
    [
        dcc.Location(id="app_url_location"),
        dcc.Store(id="f_app_loaded", storage_type="session"),
        html.Div(id="page-content",)
    ]
)


@callback(Output("f_app_loaded", "data"), Input("app_url_location", "pathname"))
def app_level_checks(app_level_path):
    print("load some data to trigger the page level callback (it doesn't work)!")
    return {"a": uuid.uuid4().hex}


@callback(
    Output("page-content", "children"),
    Input("f_app_loaded", "modified_timestamp"),
    State("f_app_loaded", "data")
)
def page_layout_cb(timestamp, data):
    print(timestamp, data)
    components = [
        html.H1("Welcome to a broken app!"),
        html.P("Please log in to continue"),
    ]
    return components


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


Step 2 Multi Page App

Next make it a multi-page app using Pages. Note that you should only have additional dcc.Location components in app.py

app.py

import dash
import flask
from dash import Dash, html, dcc, callback, Output, Input, State
import dash_bootstrap_components as dbc
import uuid

server = flask.Flask(__name__)

app = Dash(
    __name__,
    server=server,
    external_stylesheets=[dbc.themes.BOOTSTRAP],
    use_pages=True
)

app.layout = html.Div(
    [
        dcc.Location(id="app_url_location"),
        dcc.Store(id="f_app_loaded", storage_type="session"),
        dash.page_container
    ]
)


@callback(Output("f_app_loaded", "data"), Input("app_url_location", "pathname"))
def app_level_checks(app_level_path):
    print("load some data to trigger the page level callback (it doesn't work)!")
    return {"a": uuid.uuid4().hex}


@callback(
    Output("page-content", "children"),
    Input("f_app_loaded", "modified_timestamp"),
    State("f_app_loaded", "data")
)
def page_layout_cb(timestamp, data):
    print(timestamp, data)
    components = [
        html.H1("Welcome to a fixed app!"),
        html.P("Please log in to continue"),
    ]
    return components


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

pages/login.py

import dash
from dash import html


dash.register_page(__name__, path='/')


def layout(**kwargs):
    print("launch layout for login page")
    ly= html.Div(
        [
           # dcc.Location(id='page_url_location') extra dcc.Location should be only in app.py
            html.Div("login content that should be overwritten by the callback that will never trigger")

        ],
    id='page-content'
    )
    return ly



For more minimal examples for multi-page apps see:

I do see how these work, but one of the key things I want to do is avoid having page level callbacks on the app level .py file.

Is there any way to get this functionality of making the page level callbacks wait for the app level callback to be done?

I thought this was achieved by making the outputs of the app callback be the inputs of the page callback, does that only hold true when the callbacks are in the same .py file?

I’m trying to keep the pages as “children” of the app, like they are confined to the page-container which makes good sense, but then the callbacks spill out into the “parent” app level just makes organizing the code difficult.

1 Like

It doesn’t matter what file the callbacks are in.

You can control the order of the callback by using chained callbacks - where the output of one callback triggers the input of another callback.

Thanks for your help, I’ll need to explore this more…

I tried several approaches, ultimately at this time I’ve given up and just used the redis session. I wasn’t able to get the second chained callback to respect the first callback no matter what I did.

I’m also using azure AD for the login stuff so I’m in that kind of system.

I should have mentioned I’m in a multi instance cloud foundry environment with multiple files for pages.

I got what I needed working well enough for now though