Multipage "stored components"

Hi everyone!

I am a Dash practitioner and a huge advocate for the library.
These days I am trying to implement a multipage webapp and I am having a hard time to “store” changes in components when switching back and forth across pages. Namely, each time I move to a page, the layout is refreshed.

Below there is a (minimal?) code example for reproducibility purpose, where in page 1 I have a html table with the first row allowing for user input and add-validation button, then it is possible to remove the inserted row as well.
Page 2 is just a placeholder for switching back and forth.

I guess one strategy could be to have different dcc.Stores and record what happens, and populate pages from that when switching. But I hope there is a better strategy :slight_smile:

index.py

import dash
from dash import html, dcc
import dash_bootstrap_components as dbc

# Dash app
app = dash.Dash(
    __name__,
    external_stylesheets=[
        dbc.themes.FLATLY,
        dbc.icons.BOOTSTRAP,
    ],
    use_pages=True,
)

sidebar = dbc.Col(
    id="sidebar",
    children=[
        dbc.Nav(
            id="sidebar-nav",
            children=[
                dbc.NavLink(children="page1", id="page1", href="/page1", active="exact"),
                dbc.NavLink(children="page2", id="page2", href="/page2", active="exact")
            ],
        ),
    ],
)

main_content = dbc.Col(
    id="main-content",
    className="main-content-expanded",
    children=dash.page_container,
)

page = dbc.Row(
    id="page",
    children=[
        sidebar,
        main_content,
    ],
)

url = dcc.Location(id="url", refresh=False)

# App layout
app.layout = html.Div(
    id="layout",
    children=[page, url],
)

if __name__ == "__main__":
     app.run_server(
        debug=True,
        host=0.0.0.0,
        port=8080,
    )

pages/page1.py

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


dash.register_page(__name__, path="/page1", name="Page1")


def make_filled_row(button_clicked: int, name: str) -> html.Tr:
    """
    Creates a filled row in the table, with dynamic id using the button_clicked
    parameter.
    """
    return html.Tr(
        [
            html.Td(name),
            html.Td(
                html.Button(
                    "Delete",
                    id={"index": button_clicked, "type": "delete"},
                    className="delete-user-btn",
                )
            ),
        ],
        id={"index": button_clicked, "type": "table-row"},
    )


header = [
    html.Thead(
        html.Tr(
            [
                html.Th("First Name"),
                html.Th(""),
            ]
        ),
        id="table-header",
    )
]


add_row = html.Tr(
    [
        html.Td(dcc.Input(id="name-input")),
        html.Td(
            html.Button(
                "Add",
                id="add-user-btn",
            ),
            style={"width": "150px"},
        ),
    ]
)

body = [html.Tbody([add_row], id="table-body")]

table = html.Table(
    header + body,
)


# Layout of the page
layout = dbc.Container(
    [
        dbc.Row([dbc.Col([table])]),
    ]
)

# List of callbacks for the page
@callback(
    [
        Output("table-body", "children", allow_duplicate=True),
        Output("name-input", "value"),
    ],
    [
        Input("add-user-btn", "n_clicks"),
    ],
    [
        State("name-input", "value"),
    ],
    prevent_initial_call=True,
)
def on_add(n_clicks: int, name: str):
    """Adds a new row to the table, and clears the input fields"""
    table_body = Patch()
    table_body.append(make_filled_row(n_clicks, name))

    return table_body, ""


@callback(
    Output({"index": MATCH, "type": "table-row"}, "children"),
    Input({"index": MATCH, "type": "delete"}, "n_clicks"),
    prevent_initial_call=True,
)
def on_delete(n_clicks: int) -> html.Tr:
    """Deletes the row from the table"""
    if not n_clicks:
        raise PreventUpdate

    return html.Tr([])

pages/page2.py

import dash
import dash_bootstrap_components as dbc
from dash import html
dash.register_page(__name__, path="/page2", name="Page2")

# Layout of the page
layout = dbc.Container(
    [
        dbc.Row([dbc.Col(html.Div("You are on page 2"))]),
    ]
)

Welcome back!

I dealt with the same (or similliar) issue as you in my application. You actually don’t need multiple dcc.Store components for everything, I just use one dcc.Store and I have dictionary in it. As key I use pathname from dcc.Location and as value i save whole layout of the page in it. Then I just check if layout exists in dcc.Store in my page handling callback, if it does not I serve fresh new page. I also turned on persistence for each interactive element I want to persist (actually I am not sure if this is needed but haven’t tested - you know, don’t touch it if it works). Problem with this implementation is that I do not use pages (it did not existed at the time I wrote the app) and I am not sure if this implementation is even possible with pages.

Thank you for your feedback!

Currently I am aware of just two solution to this problem:

  • Store all page info in dcc.Store.
  • Hide/show content based on given url.

Both these methods address the problem by avoiding using pages, to which I wanted to give a try as they simplify some processes and led to a more modular codebase (at least in my case).

Currently I have a “fallback” solution implemented using dcc.Store, however it gets kind of slow when you move back and forth to the page, I would assume because I create components on the fly.
Now “slow” is vague and not quantified on purpose, but let me just say that the fact that the components and data are not present is very visible on switch.

One more option I didn’t investigate could be to use dcc.Store together with some caching, since the layout has to be exact same it last was - and in my case different pages inputs do not affect each other.

Interesting insight.

I do not have any performance issues with “saving” and “loading” the layout. Of course it triggers the callbacks that populate the layout with data and this can take time. But theoretically this could be eliminated (because data is already pulled from dcc.Store) one just needs to correctly identify how to stop this and I think it can be pretty complicated if you use a lot of dropdowns, checkboxes, chips etc.

I also use caching in my app. Specifically I precalculate most used layouts in the dashboard and it is really blazing fast for all users. Of course if there are a lot of options some proper caching method would be needed and it brings another kind of headaches.

1 Like

@martin2097 let me try to expand on such claim.

Ideally I would want a callback such as the following:

@callback(
    Output("table-body", "children", allow_duplicate=True),
    Input("url", "pathname"),
    State("data-store", "data")
    prevent_initial_call=True,
)
def on_page_load(pathname: str, data):
    """Re-compute table from store on page change"""
    if pathname != "/page1":
        raise PreventUpdate
    else:
    table_body = ... # here is where the magic happens ...
    return table_body

However, this is never triggered when moving from some page to page1. I don’t really understand why, possibly because the table-body component doesn’t exist at the time of calling?

Therefore I am adding some “fake” additional step/callback, but as I mentioned in the previous reply, this leads to quite slow performances:

@callback(
    Output("data-store", "data", allow_duplicate=True),
    Input("url", "pathname"),
    prevent_initial_call=True,
)
def on_page_load(pathname: str):
    if pathname != "/page1":
        raise PreventUpdate
    return Patch()

@callback(
    Output("table-body", "children", allow_duplicate=True),
    Input("table-data-store", "data"),
    prevent_initial_call=True,
)
def on_store_update(data):
    """
    Each time the store is updated, the table is updated as well.
    This results to be slow!
    """
    if data is None:
        raise PreventUpdate

    table_body = ...
    return table_body
1 Like