plotly / dash-labs

Work-in-progress technical previews of potential future Dash features.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Pages callback context issue

stevej2608 opened this issue · comments

The following snippet uses a @callback nested in within the layout() function for the page. The layout function expects an email argument that's been decoded by Dash/Pages from the query-string. The layout function is called on start-up with email=None and again when the page is rendered with email='harrysmall@gmail.com'

The problem is when the button callback is triggered the email value is None, which is the context of the first call of layout() when it should be the second.

The button callback is registered and then re-registered on the second invocation of layout() in the Dash GLOBAL_CALLBACK_MAP. The second registration should also capture the new context and this context should be active when the button callback triggers.

I've spent several hours trying to bottom out what's going on. Any ideas?

import dash

dash.register_page(__name__)

from dash import Dash, dcc, html, Input, Output, callback

# This page is invoked by:
#
#   http://localhost:8050/register?email=harrysmall%40gmail.com

def layout(email=None):

    # email here is 'harrysmall@gmail.com'

    heading = html.H2(f"Register {email}?")
    btn = html.Button("Click to register", id="btn")
    confirm = html.H2(id="confirm")

    @callback(Output("confirm", "children"), Input("btn", "n_clicks"))
    def register_cb(clicks):
        if clicks:

            # email here is None

            return f"User {email} has been registered"
        else:
            return None

    return html.Div([heading, btn, confirm])

Hi @stevej2608

Typically, the callback is not placed inside the layout function.

Try adding this to the apps in your pages folder in a file called register.py

register.py
import dash
from dash import Dash, dcc, html, Input, Output, State, callback

dash.register_page(__name__)

# This page is invoked by:
#
#   http://localhost:8050/register?email=harrysmall%40gmail.com


def layout(email=None):
    store_email = dcc.Store(id="email", data=email)
    heading = html.H2(f"Register {email}?")
    btn = html.Button("Click to register", id="btn")
    confirm = html.H2(id="confirm")

    return html.Div([heading, btn, confirm, store_email])


@callback(
    Output("confirm", "children"),
    Input("btn", "n_clicks"),
    State("email", "data"),
    prevent_initial_call=True,
)
def register_cb(clicks, email):
    if clicks:
        return f"User {email} has been registered"
    else:
        return None

Hi @AnnMarieW,

Thanks for the update. If you add the dcc.Store my nested callback works as well.

def layout(email=None):

    store_email =  dcc.Store(id="email", data=email)
    heading = html.H2(f"Register {email}?")
    btn = html.Button("Click to register", id="btn")
    confirm = html.H2(id="confirm")

    @callback(Output("confirm", "children"), Input("btn", "n_clicks"), State("email", "data"), prevent_initial_call=True)
    def register_cb(clicks, email):
        if clicks:
            return f"User {email} has been regisered"
        else:
            return None

    return html.Div([heading, btn, confirm, store_email])

I've done a bit more poking around the Dash code. The problem I've experienced is as a result of the callback and the context being baked in on the first invocation of layout(). The following invocations result in the callback & new context being saved in the GLOBAL_CALLBACK_MAP. The problem is this map is read once by Dash when the server starts and is ignored thereafter. The new context is never invoked.

If this could be addressed the dcc.Store work around would not be needed.

Cheers.

Hi @stevej2608
You could avoid the dcc.Store by giving the heading an id and parsing the children prop in the callback to get the email.

I confirmed with the Plotly team that putting callbacks inside the layout function like in your example is a Dash anti-pattern that is not supported. All callbacks must be defined before the server starts.

With multi-process servers and/or multiple users looking at the app: they all need to have the same understanding of what callbacks exist and what the global state is, irrespective of what callbacks have already been executed.