Sharing components between pages

I am working on an application where the routing between pages has been implemented by adding elements to the layout initially, and then changing their visibility based on the url. In principle, a structure like this,

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

app = Dash()
app.layout = html.Div([
    html.Div(["Page1", dcc.Link("Go to page2", href="/page2")], id="page1"),
    html.Div(["Page2", dcc.Link("Go to page1", href="/page1")], id="page2", hidden=True),
    dcc.Location(id="location") 
])

@app.callback(Output("page1", "hidden"),
              Output("page2", "hidden"),
              Input("location", "pathname"))
def navigate(pathname):
    if "page2" in pathname:
        return [True, False]
    return [False, True]

if __name__ == '__main__':
    app.run_server(port=9999)

While this approach works, it quickly grows in complexity, and the application is already at a point where people are loosing the overview. I was thinking about migrating the app to use Dash pages, as this seems like a much cleaner and more scaleable approach.

However, the initial approach has one main advantage from the user perspective: The state is kept when the user navigates between pages. Hence, selections in drop downs are remembered, tables remain populated and so on. I know that (some of) this behavior can be achieved using persistence, but that requires a lot more work/configuration. And I am not sure that it will be suitable for large amounts of data (e.g. a large aggrid).

To make matters more challenging, some component are also shared between pages. It could be e.g. a large aggrid, that is used on multiple pages, and which takes a long time (several seconds) to populate. With the pages approach, I need to populate the grid on every page change - but with the toggle visbility approach, this can be avoided (at the cost of even more complex navigation code).

In essence, I don’t see any (easy) way of keeping the user experience intact, if we migrate to Dash pages. Has anyone faced similar challenges? How did you address them? :slight_smile:

I just started a Dash PR to be able to add more data passed to the layout functions of the pages routing. This would allow to have a central app state Store that passes all config to pages layout functions.

This doesn’t answer your question of the large datable though. For this one I would probably have the data in a Store or hidden table in the main layout then something that syncs the data clientside when you use it in the different pages.

3 Likes

@RenaudLN that looks nice! However, as you note, it won’t solve this issue with the large datatable. I believe clientside sync with a Store will still be too slow, as it takes (significant) time just to render the datatable.

I have created a small extension to dash pages that enables dynamic components, i.e. components where the visibility depends on the URL. Similar to pages, you add the parent container to the app layout,

from dash import html, page_container
from dash_extensions.pages import setup_dynamic_components
...

app.layout = html.Div([
    ...
    dash.page_container,
    setup_dynamic_components()
])

and pass the component(s) as part of the page registration,

import dash
from components import my_data_table

dash.register_page(__name__, components=[my_data_table])

to indicate which component(s) should be visible on this page. I have only done a bit of testing, but it seems to work as intended. You can try it if you want with by installing dash-extensions==1.0.4rc1. If there is interest, I guess this functionality could be turned into a PR to Dash pages :slight_smile:

The main drawback of this solution is that you can’t mix dynamic components with the layout of your (Dash pages) page; you can only place them above or below. The only possible workaround I have found would be to use CSS Grid for positioning; but as I understand, that would only work, if the dynamic components are located inside the dash_pages container (all the grid elements must have the same parent). And to do that, the pages update mechanism would need to be changed a partial update (to avoid reloading the dynamic components).

Hi @RenaudLN and @Emil

These are great new features for Pages!

@Emil if it doesn’t work to put the dynamic component (such as a large grid) above or below dash.page_container how about making the dynamic component the entire layout for the page?

dash.register_page(__name__, components=[layout], layout=html.Div())


1 Like

Hi @Emil,

a quick question about the components argument in dash.register_page(__name__, components=[my_data_table]).

What gets passed to components?

I was thinking it would be callback output ID’s i.e. if the callback looked something like this:

@callback([Output('plotting_complete_message', 'children'),
           Output('plot_1', 'figure'),
           Output('plot_2', 'figure')],
          [Input('dataframe_1', 'data')])
  
def plot_generator_1(dataframe_1):
       # Check if input does not contain data and prevent app update.
       if dataframe_1 is None:
            raise PreventUpdate

       # Check if input contains data and trigger callback.
       elif dataframe_1 is not None:
            some code that returns plot_1 and plot_2
 
            plotting_complete_message = html.H3("Complete")

            return(plotting_complete_message, plot_1, plot_2)
       else:
            return(no_update, no_update, no_update)

Would the component(s) then be passed as follows:

Page 1:
dash.register_page(__name__, components=['plotting_complete_message'])

Page 2:
dash.register_page(__name__, components=['plot_1', 'plot_2'])

In this example the outputs of the callback are split between pages.

The wider back-ground here is that I’m migrating my 1 page multi-tab Dash dasboard to a Dash multi-page with mutli-tab web-app/dashboard. I’ve got an open question on this as I’m working on various challenges:

https://community.plotly.com/t/a-nonexistent-object-was-used-in-an-output-of-a-dash-callback-correct-use-of-multi-page-app-validation/78864/6

My web-app has a Data Upload page (Dash-uploader button used to drop zipped files) that contains a layout updated dynamically with “Complete” messages as various callbacks finish. These callbacks are as above, not only outputting the “Complete” message, but also the plots which are served to a Visualisation page. In my 1 page multi-tab approach, as “Complete” messsages appear for plots in upload tab, it’s then possible to see them in the visualisation tab. Clicking between tabs whilst this process is happening doesn’t refresh tab content and so it’s just to check progress.

In the multi-page app, I would like the same functionality. However, I get a refresh of page content as callbacks are re-initiated for plotting, when I click on the Visualisation page and vice versa. Still trying to understand, where in my callback chain this happens and how it relates to pages.

Thanks for your time.

Alex