Data persistence in a multi-page Dash app

38 Views Asked by At

I have a Dash app where user in one page, data.py, adds some data, and later selected rows can be viewed and removed on another page, grid.py. The user should be able to later get back to data.py and add some more data.

The problem: data is not persisted between the visits to the grid.py. How can I achieved that? I tried setting persistence property, but that didn't get me anywhere. existing_data in grid.py is always None. When I use a single-page app, similar code works.

Here's my minimal reproducible example:

app.py

from dash import html, dcc
import dash

app = dash.Dash(__name__, use_pages=True)

app.layout = html.Div(
    [
        dcc.Store(id="store", data={}),
        html.H1("Multi Page App Demo: Sharing data between pages"),
        html.Div(
            [
                html.Div(
                    dcc.Link(f"{page['name']}", href=page["path"]),
                )
                for page in dash.page_registry.values()
            ]
        ),
        html.Hr(),
        dash.page_container,
    ]
)


if __name__ == "__main__":
    app.run_server(debug=True)

data.py

from dash import html, Input, Output, callback, register_page
from dash.exceptions import PreventUpdate
import random

register_page(__name__, path="/")


layout = html.Div(
    [
        html.H3("Data input"),
        html.Button("Add row", id="button_id"),
        html.Br(),
        html.Div(id="my-output"),
    ]
)


@callback(
    [Output("store", "data"), Output("my-output", "children")],
    Input("button_id", "n_clicks"),
    prevent_initial_call=True
)
def add_data(n_clicks):
    if n_clicks:
        new_data = [{"col1": "New row", "col2": random.randint(0, 1000)}]
        return new_data, html.Pre(str(new_data))
    else:
        raise PreventUpdate

grid.py

from dash import html, dash_table, Input, Output, callback, register_page, State


register_page(__name__)


layout = html.Div([
    html.H3("Data tables"),
    dash_table.DataTable(
        id="table",
        row_deletable=True,
        column_selectable="single",
        page_size=5,
        persistence=True,
        persisted_props=[
            "data",
            "columns.name",
            "filter_query",
            "hidden_columns",
            "page_current",
            "selected_columns",
            "selected_rows",
            "sort_by",
        ],
    ),
])


@callback(
    Output("table", "data"),
    Input("store", "data"),
    State("table", "data"),
)
def update(new_data, existing_data):
    if existing_data is not None:
        return existing_data + new_data
    else:
        return new_data
1

There are 1 best solutions below

2
Lukasz Tracewski On

I have figured out two ways of doing it:

  1. Shared Store component.
  2. Using a persistent component feature from dash_extensions.pages.

I am going to share both for completeness. One reason for having (2) is that I do not fully understand why (1) works, as to my knowledge it introduces a circular dependency. The latter should throw an exception or, worse, result in an infinite callback cycle, which it doesn't. That's smart on behalf of Dash, but I can't be certain it's going to work always - it's not documented.

Shared Store component

app.py:

from dash import html, dcc
import dash

app = dash.Dash(__name__, use_pages=True)

app.layout = html.Div(
    [
        dcc.Store(id="store", data=[], storage_type="session"),
        html.H1("Multi Page App Demo: Sharing data between pages"),
        html.Div(
            [
                html.Div(
                    dcc.Link(f"{page['name']}", href=page["path"]),
                )
                for page in dash.page_registry.values()
            ],
        id='navbar'),
        html.Hr(),
        dash.page_container
    ]
)


if __name__ == "__main__":
    app.run_server(debug=True)

pages/data.py

from dash import html, Input, Output, callback, register_page, State
from dash.exceptions import PreventUpdate
import random

register_page(__name__, path="/")


layout = html.Div(
    [
        html.H3("Data input"),
        html.Button("Add row", id="button_id"),
        html.Br(),
        html.Div(id="my-output"),
    ]
)


@callback(
    [Output("store", "data"), Output("my-output", "children")],
    Input("button_id", "n_clicks"),
    State("store", "data")
)
def add_data(n_clicks, data):
    if n_clicks:
        new_data = [{"col1": "New row", "col2": random.randint(0, 1000)}]
        return data + new_data, html.Pre(str(new_data))
    else:
        raise PreventUpdate

pages/grid.py:

from dash import html, dash_table, Input, Output, callback, register_page


register_page(__name__)


layout = html.Div([
    html.H3("Data tables"),
    dash_table.DataTable(
        id="table",
        data=[{"name": "Test", "label": "Test"}],
        row_deletable=True,
        column_selectable="single",
        page_size=5,
        persistence=True,
        persisted_props=["columns.name", "data"],
    ),
])


@callback(
    Output("table", "data"),
    Input("store", "data"),
)
def add_rows(new_data):
    return new_data

@callback(
    Output("store", "data", allow_duplicate=True),
    Input("table", "data"),
    prevent_initial_call=True
)
def update_back(new_data):
    return new_data

Using a persistent component feature from dash_extensions.pages

app.py:

from dash import html, dcc
from dash_extensions.pages import setup_page_components
from pages.components import NAVBAR_ID
import dash

app = dash.Dash(__name__, use_pages=True)

app.layout = html.Div(
    [
        dcc.Store(id="store", data=[], storage_type="session"),
        html.H1("Multi Page App Demo: Sharing data between pages"),
        html.Div(
            [
                html.Div(
                    dcc.Link(f"{page['name']}", href=page["path"]),
                )
                for page in dash.page_registry.values()
            ],
        id=NAVBAR_ID),
        html.Hr(),
        dash.page_container,
        setup_page_components()
    ]
)

if __name__ == "__main__":
    app.run_server(debug=True)

pages/components.py

from dash import dash_table

NAVBAR_ID = "navbar"

data_table = dash_table.DataTable(
        id="table",
        row_deletable=True,
        column_selectable="single",
        page_size=5,
        persistence=True,
        persisted_props=[
            "data",
            "columns.name",
        ]
    )

pages/data.py

from dash import html, Input, Output, callback, register_page
from dash.exceptions import PreventUpdate
import random

register_page(__name__, path="/")


layout = html.Div(
    [
        html.H3("Data input"),
        html.Button("Add row", id="button_id"),
        html.Br(),
        html.Div(id="my-output"),
    ]
)


@callback(
    [Output("store", "data"), Output("my-output", "children")],
    Input("button_id", "n_clicks")
)
def add_data(n_clicks):
    if n_clicks:
        new_data = [{"col1": "New row", "col2": random.randint(0, 1000)}]
        return new_data, html.Pre(str(new_data))
    else:
        raise PreventUpdate

pages/grid.py

from dash import html, Input, Output, callback, register_page, State
from pages.components import data_table


register_page(__name__, page_components=[data_table])


layout = html.Div([
    html.H3("Data tables"),
])


@callback(
    Output("table", "data"),
    Input("store", "data"),
    State("table", "data")
)
def add_rows(new_data, old_data):
    if old_data:
        return old_data + new_data
    return new_data