Select component being updated at page change regardless of persistence and PreventUpdate options

I have a multipage Dash app and started using the persistence=True option to prevent some components to be updated when I change the page (or even when I refresh the browser). However, there is a specific Select component (from dash bootstrap) that is refusing to obey.

The component looks simply like this

dbc.Select(
    id="locations",
    persistence=True
),

There is a single callback that can update the options and value attributes of this component and it is specifically told to prevent the update in case there is no click recorded, that is

@callback(
    [Output("locations", "options"),
     Output("locations", "value")],
    Input("search-button", "n_clicks"),
    State("locations-list", "data")
    prevent_initial_call=True
)
def get_closest_address(n_clicks, from_address):
    if n_clicks is None:
        raise PreventUpdate

I did verify that I’m getting a PreventUpdate every time that I change page, BUT, if I look into the callback graph I can see that the options and value attributes of the component are indeed changed.

Here is a video that shows the behaviour

ezgif.com-video-to-gif

So what could be the reason behind the attributes update? I’m just trying to have this component not to reset every time I change the page.

Hello @guidocioni,

Persistence only works with user adjustments, and will break if it is adjusted through a callback.

Then workaround would be for you to make your own by saying if the options or value change, you store it into a dcc.Store and then pass the store value to the component when it first loads.

Even if the callback raises a PreventUpdate?

PreventUpdate wouldn’t update the value and options, so, no this shouldn’t break it.

But if you are wanting persistence from the system adjustments, this isn’t possible currently.

Mmm so now I’m confused. If PreventUpdate does not update the attributes of locations, what is actually updating them? You mention “system adjustments” but I’m not sure I understand what you mean.
Thanks :slight_smile:

The problem isn’t with the first callback on the page load, it is that any changes (by a returned value from the callback) to those properties will not be sustained upon reload.

And a page change counts as reload?

Any time the component would be reloaded from the server, yes…

If you want more help, or more details, as to why yours isn’t working. Please post your app or a minimum reproducible application.

Ok I see. The thing is I’m sharing the same component in every page, but somehow this is still triggering a reload.

I’ll try to implement the workaround the cited at first, seems like a good idea to store the value in a Store component and to manually update it.

I didn’t post the app only because it is quite large and wanted to keep the code to a minimum in order not to waste anyone’s time or confuse.
But I don’t have any problem sharing it. The app is here GitHub - guidocioni/point_wx and the component I’m using is defined here https://github.com/guidocioni/point_wx/blob/main/src/components/location_selector.py and used here https://github.com/guidocioni/point_wx/blob/36114e56242a1b1e91acedba03599b651758dce9/src/pages/ensemble/callbacks.py#L17.
Doesn’t have any hard dependency so you should be able to run it locally.

Hello @guidocioni,

Looking at your code, you seem to only be driving this info from the callback, and your options are blank, the only persistence can be the value:

persisted_props (list of a value equal to: 'value’s ; default ['value'] ): Properties whose user interactions will persist after refreshing the component or the page. Since only value is allowed this prop can normally be ignored.

Therefore, it is impossible to persist and show your value as your options are completely blank upon load. XD

Thanks for taking a look.

Ok so the options will always be blank regardless. I think I need an additional callback which runs at every refresh at forces an update of the drop down options using the JSON contained in the Store object. If I add persistence to the Store object (which holds the JSON In the value attribute).it should work.

Hello @guidocioni,

I’d recommend using the id of the Select as a trigger, then load the options and value from the dcc.Store, then on the other callback, allow_duplicate on the options and value, and also have where those things update in the dcc.Store upon options and value of the Select being updated.

This should keep you out of the realm of circular callbacks.

But the id of the Select only updates at page load, right? It will never change on page refresh.

This is the prototype of the additional callback I added

@callback(
    [Output("locations", "options", allow_duplicate=True),
     Output("locations", "value", allow_duplicate=True)],
    Input("locations", "id"),
    State("locations-list", "data"),
    prevent_initial_call=True
)
def update_locations_list(_nouse, locations):
    locations = pd.read_json(locations, orient='split', dtype={"id": str})
    # for now no other action

This is obviously never triggered, because prevent_initial_call=True and id never changes.

I tried to use the same input trigger also in this callback, i.e. Input("search-button", "n_clicks"), but also this didn’t work.

I really don’t get why the original callback in get_closest_address triggers at every page refresh if the only Input is given by n_clicks of search-button, and this does not change…

Sorry, in the end since get_closest_address was called every time I ended up doing something even simpler without an additional callback.

@callback(
    [Output("locations", "options"),
     Output("locations", "value"),
     Output("locations-list", "data")],
    Input("search-button", "n_clicks"),
    [State("from_address", "value"),
     State("locations-list", "data")]
)
def get_closest_address(n_clicks, from_address, locations):
    if n_clicks is None:
        # In this case it means that the button has not been clicked
        # so we first check if there are already some locations
        # saved in the cache
        # If there is no data in the cache, locations will be an empty dict
        if len(locations) > 0:
            locations = pd.read_json(
                locations, orient='split', dtype={"id": str})
        else:
            # In this case it means the button has not been clicked AND
            # there is no data in the Store component
            raise PreventUpdate
    else:
        # In this case the button has been clicked so we load the data
        locations = get_locations(from_address)

    options = []
    for _, row in locations.iterrows():
        options.append(
            {
                "label": (
                    f"{row['name']} ({row['country']} | {row['longitude']:.1f}E, "
                    f"{row['latitude']:.1f}N, {row['elevation']:.0f}m)"
                ),
                "value": str(row['id'])
            }
        )
    if n_clicks is None:
        # In this case we need to update everything besides the value
        # because it is persisted, so if the user already selected something
        # it will be there
        return options, no_update, locations.to_json(orient='split')
    else:
        # If we're here, it means the button has been clicked, so we need
        # to update everything, and set the value as the first option (default)
        return options, options[0]['value'], locations.to_json(orient='split')

Note that I removed the prevent_initial_call because this should be evaluated every time the page refreshes or the button is pressed.

Every time I get into the callback I check if the button has actually been pressed. If yes, I then proceed to check if there is already data in the Store component, which is locally persisted. If yes, then I use that data to populate the options of the Select component. I do not update the value attribute in this case because this is also persisted, so I get whatever was already selected before.
If there is no data in Store and the button has not been pressed I don’t do anything.
Otherwise, if the button has been pressed I compute again the locations and populate both options and value attributes of the Select.

I don’t know how clean this is, but it seems to work pretty well.
There is still something weird:

  • I still don’t understand why this callback is called every time the page is refreshed if the Input is only n_clicks, but I guess it’s because the layout is served every time…
  • If the user does not select the option in the Select then the value property is not persisted, even though it is indeed set. maybe it is because setting via callback does not allow it to be persisted? I don’t know
1 Like

You could always set the options in the component.

The component is being readded each time you change pages.

Dash pages is a fetch request, which responds with a json object that gets iterated through with the dash renderer, which renders the response into _pages_content. So, even though the component appears to not move or whatever, it is being rendered all over again in the new server response.