plotly / dash-labs

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

Multi-Page Apps - find a better way to build dash.page_registry #76

Open AnnMarieW opened 2 years ago

AnnMarieW commented 2 years ago

Currently app pages are added to the dash.page_registry when dash.register_page is called. This happens as modules are loaded from the pages/ folder before the app starts. If you try to use dash.page_registry in an app in the pages/ folder, you must use it in a function, otherwise the page registry may not be complete.

Need to find a better way to build dash.page_registry so it's not necessary for people to use a function when trying to use it from within the pages/ folder.

See community discussion here

Description of the workaround here

AnnMarieW commented 2 years ago

Here is a potential solution as outlined by @ chriddyp The solution will be a pull request in dash instead of here in dash-labs

Have page_registry values return a function that returns the value. So instead of:

>>> page_registry['historical']['href']
`/the-thing`

it’d be:

>>> page_registry['historical']['href']()
`/the-thing`

meaning:

>>> page_registry['historical']['href']
<function ...>
  1. Dash users would still use page_registry['historical']['href'] within their code, but the layouts would now contain functions instead of values. So pages/some_other_page.py could have:
    layout = dcc.Link('Return home', 
    href=page_registry['home']['href'])

where href would be a function, not the value!

  1. Then, change Dash’s serializer to handle functions. So when Dash turns Python objects into JSON strings, if it encountered a function it would simply call the function and serialize the value that it got in return

I suspect you’ll be able to do it here: https://github.com/plotly/dash/blob/dev/dash/development/base_component.py#L206

and here: https://github.com/plotly/dash/blob/dev/dash/development/base_component.py#L213

AnnMarieW commented 2 years ago

It looks like the above solution may not be ideal:

For example, let's say you would like to create a "topics" sub-menu from within the pages/ folder.

In the proposed solution, the page["path"] is a function. This mean's that you can't create it like below by looping through dash.page_registry because page["path"] has no attribute 'startswith'

        for page in dash.page_registry.values()
        if page["path"].startswith("/topic")

But if you use the value of the function, then we are back to the same issue. The dash.page_registry is not finished building yet, so the sub-menu will not be complete.

        for page in dash.page_registry.values()
        if page["path"]().startswith("/topic")

@chriddyp do you have any other comments or suggestions?

alexcjohnson commented 2 years ago

For reference, a note I wrote in a DM with @AnnMarieW - the upshot is I don't think we need to worry about this right now but we could consider adding another mechanism in the future if it becomes a big point of friction.

The only solution I see is to move the dash.register_page calls outside the page files so you can ensure they all run first… and that kind of defeats the purpose.

I suppose there would be a way by converting the dash.register_page call into a comment, we could define the format but something like:

# register_page
# path_template="/a/<id_a>/b/<id_b>"
# id="register_page

and then we first read the file and create all the page_registry entries with that information first, then attach the modules to each entry later. But that seems like an awfully cumbersome & fragile thing to try to teach people, just so they can avoid putting some things in functions occasionally.