dash_table.DataTable persistence not compatible with navlinks

Viewed 108

I'm trying to preserve values in a dash_table.DataTable after switching between apps in an multi page Dash app. I've tried with both navlinks (example code below), pages (dash 2.5) and tabs. I want to do this using the persistence argument instead of dcc.Store, and with navlinks/pages instead of tabs.

It seems refreshing the page overrides the persistence keyword, even when set to 'session' or 'local', as using tabs fixes this issue (tabs does not refresh page). I've also tried dcc.Location with refresh=False without success. I've made a small example code to display my issue. From my print statements you can also observe the data beeing reset to 'None' after switching between links/pages.

import pandas as pd
import numpy as np
from dash import Dash, callback, html, dcc, dash_table, Input, Output, dash_table, callback_context as ctx
import dash_bootstrap_components as dbc


df = pd.DataFrame({'test1':np.zeros(3), 'test2':np.zeros(3)})
def get_data():
    return df

app = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP], suppress_callback_exceptions= True)
app.layout = dbc.Container([
    dcc.Location(id = 'url', refresh = False),
    dbc.Row(
        dbc.Navbar(
            dbc.Nav(children=[
                dbc.NavItem(dbc.NavLink("Main", active='exact', external_link=False, href='/'), id = 'main'),
                dbc.NavItem(dbc.NavLink("A link", active = 'exact', external_link=False, href="/link"), id = 'alink')
            ])
        )
    ),
    html.Div(id = 'link')
])
    
main = html.Div([
            dbc.Row(
                dbc.Col([
                    dbc.Button("test1 +1", id='btn1', className="me-2", n_clicks = 0),
                    dbc.Button("test2 +1", id='btn2', className="me-2", n_clicks = 0)
                ])
            ),
            dbc.Row([
                dbc.Col(
                    dash_table.DataTable(columns = [{'name': x, 'id':x} for x in get_data().columns],
                                         id = 'table',
                                         persistence = True, 
                                         persisted_props = ['data'],
                                         persistence_type = 'session')
                )
            ])
    ])

@app.callback(
    Output('link', 'children'),
    Input('url', 'pathname')
)
def render_page(path):
    print(path)
    if path != '/':
        layout = html.Div(
            dbc.Row(
                html.H5(path)
            )
        )
    else:
        layout = main
    return layout

@app.callback(
    Output('table', 'data'),
    [
     Input('table', 'data'),
     Input("btn1", "n_clicks"),
     Input("btn2", "n_clicks")
    ]
)
def render_table(data,b1,b2):
    print("Data: ", data)
    if data == None:
        return get_data().to_dict('records')
    
    df = pd.DataFrame(data)
    button_id = ctx.triggered[0]["prop_id"].split(".")[0]
    
    if button_id == 'btn1':
        df['test1'] += 1
    elif button_id == 'btn2':
        df['test2'] += 1
    return df.to_dict('records')

app.run_server()
1 Answers

Every time that pathname triggers the first callback and directs the app to the main page, your DataTable is reseted as the df defined in the beggining of the code. That's why simply setting the table persistence doesn't help, you're creating it from zero each time.

If you don't want to use dcc.Store, you need to create your table only once and use pathname triggering for deciding whether you show the table or not. That's what I did with your code, I defined the table in the main layout, and when url is not in the main page, a {'display': 'none'} argument is passed for the table parent div.

import pandas as pd
import numpy as np
from dash import Dash, callback, html, dcc, dash_table, Input, Output, dash_table, callback_context as ctx
import dash_bootstrap_components as dbc


df = pd.DataFrame({'test1':np.zeros(3), 'test2':np.zeros(3)})
def get_data():
    return df

app = Dash(external_stylesheets=[dbc.themes.BOOTSTRAP], suppress_callback_exceptions= True)
app.layout = dbc.Container([
    dcc.Location(id = 'url', refresh = False),
    dbc.Row(
        dbc.Navbar(
            dbc.Nav(children=[
                dbc.NavItem(dbc.NavLink("Main", active='exact', external_link=False, href='/'), id = 'main'),
                dbc.NavItem(dbc.NavLink("A link", active = 'exact', external_link=False, href="/link"), id = 'alink')
            ])
        )
    ),
    html.Div(id = 'link'),
    
    html.Div(id = 'table_div', children = [
            dbc.Row(
                dbc.Col([
                    dbc.Button("test1 +1", id='btn1', className="me-2", n_clicks = 0),
                    dbc.Button("test2 +1", id='btn2', className="me-2", n_clicks = 0)
                ])
            ),
            dbc.Row([
                dbc.Col(
                    dash_table.DataTable(columns = [{'name': x, 'id':x} for x in get_data().columns],
                                         id = 'table',
                                         persistence = True, 
                                         persisted_props = ['data'],
                                         persistence_type = 'session')
                )
            ])
    ])
])

@app.callback(
    Output('link', 'children'),
    Output('link', 'style'),
    Output('table_div', 'style'),
    Input('url', 'pathname')
)
def render_page(path):
    print(path)
    if path != '/':
        layout = html.Div(
            dbc.Row(
                html.H5(path)
            )
        )
        return layout, {'display': ''}, {'display': 'none'}
    else:
        # layout = main
        return [], {'display': 'none'}, {'display': ''}

@app.callback(
    Output('table', 'data'),
    [
     Input('table', 'data'),
     Input("btn1", "n_clicks"),
     Input("btn2", "n_clicks")
    ]
)
def render_table(data,b1,b2):
    print("Data: ", data)
    if data == None:
        return get_data().to_dict('records')
    
    df = pd.DataFrame(data)
    button_id = ctx.triggered[0]["prop_id"].split(".")[0]
    
    if button_id == 'btn1':
        df['test1'] += 1
    elif button_id == 'btn2':
        df['test2'] += 1
    return df.to_dict('records')

app.run_server()
Related