plotly / dash-labs

Work-in-progress technical previews of potential future Dash features.
MIT License
139 stars 39 forks source link

Pages callback context issue #92

Closed stevej2608 closed 2 years ago

stevej2608 commented 2 years ago

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])
AnnMarieW commented 2 years ago

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
stevej2608 commented 2 years ago

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.

AnnMarieW commented 2 years ago

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.

AnnMarieW commented 2 years ago

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.