How to Keep Multiple Components Synchronized ACROSS Pages

Hi all,

I am experimenting with a basic Dash app that has two pages, each containing a single dropdown. I want these dropdowns to remain synchronized at all times, functioning like ‘Vavien dropdowns’. However, the way multiple pages are handled in Dash seems to make this task quite challenging. Below is my minimal working example:

app.py:

import dash
from dash import Dash, html, dcc

app = Dash(__name__, suppress_callback_exceptions=True, use_pages=True)
server = app.server


app.layout = html.Div([
    html.H1('Multi-page app with Dash Pages'),
    html.Div([
        html.Div(
            dcc.Link(f"{page['name']}",
                     href=page["relative_path"])
        ) for page in dash.page_registry.values()
    ]),
    dash.page_container,
    dcc.Store(id='between-pages', storage_type='memory'),  # Shared state store
])


if __name__ == '__main__':
    app.run(debug=True)

dropdown1.py:

from dash import Input, Output, dcc, html, callback, register_page, State, no_update
from dash import callback_context as ctx
from dash.exceptions import PreventUpdate


register_page(__name__, path="/", name="Dropdown1")

dropdown_options = ['A', 'B', 'C']

layout = html.Div([
    dcc.Dropdown(
        id='dropdown1',
        persistence=True,
        persistence_type='session',
        options=[
            {'label': i, 'value': i}
            for i in dropdown_options],
    )
])


@callback(
    [Output('dropdown1', 'value'),
     Output('between-pages', 'data')
     ],
    [Input('dropdown1', 'value'),
     Input('between-pages', 'data')
     ],
)
def sync_dropdown_and_store(dd1_value, store_value):
    trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]
    value = store_value if trigger_id == "between-pages" else dd1_value
    return value, value

dropdown2.py:

from dash import Input, Output, dcc, html, callback, register_page, State
from dash import callback_context as ctx

register_page(__name__, name="Dropdown2")

dropdown_options = ['A', 'B', 'C']

layout = html.Div([
    dcc.Dropdown(
        id='dropdown2',
        persistence=True,
        persistence_type='session',
        options=[
            {'label': i, 'value': i}
            for i in dropdown_options],
    )
])
# Initialize dropdown value from shared state


@callback(
    Output('dropdown2', 'value'),
    [Input('between-pages', 'data')],
    State('dropdown2', 'value')
)
def initialize_dropdown2(store_value, dd2_value):
    if store_value is not None and store_value != dd2_value:
        return store_value
    return dd2_value


@callback(
    [Output('dropdown2', 'value', allow_duplicate=True),
     Output('between-pages', 'data', allow_duplicate=True)
     ],
    [Input('dropdown2', 'value'),
     Input('between-pages', 'data')
     ],
    prevent_initial_call=True

)
def sync_dropdown_and_store(dd2_value, store_value):
    trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]
    value = store_value if trigger_id == "between-pages" else dd2_value
    return value, value

Additionally, I am encountering an issue with suppress_callback_exceptions. Despite setting it to True, I am still receiving the error: “A nonexistent object was used in an Input of a Dash callback…” for the component already present in the layout at the app’s initialization.

I would appreciate any advice or solutions to make my minimal example work as intended.

Looking forward to your suggestions!

Hello @Berbere,

Have you looked into @Emil solution for this?

https://www.dash-extensions.com/sections/pages

Thank you, @jinnyzor, for your recommendation. @Emil’s solution is indeed great. However, with this “example” app, my goal is to understand how things work with Dash pages and whether it is possible to achieve the synchronization I mentioned in my post.

Therefore, my question is more about whether it is possible to accomplish this kind of synchronization across pages, rather than just seeking help to make this app. @Emil mentions in his introduction to pages that “You can, of course, share component definitions in terms of code, but the component itself is re-created each time the user navigates to a new page. Hence, you’ll need to do any state synchronization yourself.”

I am curious about how to make the state synchronization indicated in my post possible. If it is indeed not possible, I aim to understand why that is the case.

It is possible - it’s just a bit tedious :wink: One possible design would be to add store component(s) at the top level of your app (so that they remain present across all pages). You could then save the state to these stores from each page (using callbacks), and read the (most recent state) when a page is loaded.

2 Likes

@Emil,

Believe that’s what he was trying…

The issue is that his callbacks are t structured properly.

@Berbere, instead of placing the callbacks to have two outputs, it should be something like this:

value changes → store update (allow_duplicate)

Then on page load, component id being added, pull from the store.

id add / state store → update the value of the component.

1 Like

Thank you so much @jinnyzor. I redesigned my callback in the light of your suggestions.
I am sharing here my solution here in case someone needs it at some point.

app.py stays the same.

dropdown1.py:

from dash import Input, Output, dcc, html, callback, register_page, State, no_update, ALL
import dash
from dash.exceptions import PreventUpdate


register_page(__name__, path="/", name="Dropdown1")

dropdown_options = ['A', 'B', 'C']

layout = html.Div([
    dcc.Dropdown(
        id='dropdown1',
        # persistence=True,
        # persistence_type='session',
        options=[
            {'label': i, 'value': i}
            for i in dropdown_options],
    )
])


@callback(
    Output('between-pages', 'data', allow_duplicate=True),
    Input('dropdown1', 'value'), prevent_initial_call=True
)
def keep_value_stored(dd1_value):
    return dd1_value


@callback(Output('dropdown1', 'value'),
          Input('url', 'pathname'),
          State('between-pages', 'data'))
def initialize_dropdown1(pathname, value):
    if pathname == '/':
        print('triggered')
        return value
    # Do nothing if the pathname is not '/Dropdown2'
    raise dash.exceptions.PreventUpdate

dropdown2.py:

from dash import Input, Output, dcc, html, callback, register_page, State, ALL
import dash
register_page(__name__, name="Dropdown2")

dropdown_options = ['A', 'B', 'C']

layout = html.Div([
    dcc.Dropdown(
        id='dropdown2',
        # persistence=True,
        # persistence_type='session',
        options=[
            {'label': i, 'value': i}
            for i in dropdown_options],
    )
])
# Initialize dropdown value from shared state


@callback(
    Output('between-pages', 'data'),
    Input('dropdown2', 'value'),
)
def keep_val_stored2(dd2_value):
    return dd2_value


@callback(Output('dropdown2', 'value'),
          Input('url', 'pathname'),
          State('between-pages', 'data'))
def initialize_dropdown2(pathname, value):
    if pathname == '/dropdown2':
        return value
    # Do nothing if the pathname is not '/Dropdown2'
    raise dash.exceptions.PreventUpdate
1 Like