Query string in multi pages app not compatible with persistent spanning-pages input parameter

The scenario is a multi-pages app with an input parameter (dcc.Input with a given id) being present in more than one page, so that if you change the parameter in one page the change is reflected in all pages, because the parameter is memory persistent so that its value is lost on refresh but kept when switching between pages.

But what if I want to add query string initialization of the dcc.Input parameter? The query string value is passed to the layout function as an argument,

def layout(my_parameter = <some_default_value>):
    return [
        ...
        input_my_parameter(my_parameter),
        ...
    ]

where, as far as I understand, since I cannot define in two different layouts two dcc.Input elements with the same id, I have to enclose the dcc.Input definition in a pre-defined function that accepts the query_string_value as argument and returns the dcc.Input element with the value property set to query_string_value, as below;

def input_my_parameter(query_string_value):
    return dcc.Input(
        id='some_id',
        type="number",
        value=float(query_string_value),
        persistence = True,
        persistence_type = 'memory'
    )

With this solution you correctly get the parameter set to query string value when using query string in URL and set to default value when not, but when you switch page persistence is broken because layout receives None as value of my_parameter which therefore is set to default value instead of keeping current value.

Any clue on how to solve this issue?
Thanks in advance for your attention.

Hi,

I am not sure if I understand your problem. If you want to use my_parameter in various pages of your app, you could store it in a dcc.Store() and retrieve the value once the page has been selected.

As @AIMPED suggested, using dcc.Store should work. Hereā€™s an example of doing this with Pages - See example #11 and Example #13

1 Like

I guess that those examples (at least #13) use dcc.Store for didactic purposes, in fact dcc.Store in general is not needed to share value of dcc.Input parameters between pages, since persistency by memory fits perfectly such need; the problem Iā€™m reporting arises when you want to use query strings too (which forces you to enclose dcc.Input definition inside a function, as far as I understand).
Also, I already use dcc.Store to store and share processed data, but using it to store data already stored as value of dcc.Input seems to me a workaround that I do not want to follow, also because it would increase the complexity of my code, Iā€™d rather drop altogether the use of query strings.

However, what Iā€™m reporting here, is that the use of query strings and page change in combination with persistency might have unveiled a Dash issue with persistency (by memory, at least); in fact percistency by memory seems unable to supersede dcc.Input value on page change, as expected (as far as I understand).

Let me know if I was clear enough.

To be clearer I have refactored your example#13 using persistence (the way Iā€™m using it in my app) instead of dcc.Store to sync a parameter common to more pages:

As you may see, using persistence the code is simpler. Starting from this I will add as soon as possible another example using query strings.

@nopria Thanks for this solution!
Youā€™re right itā€™s a much better way. Iā€™d welcome a pull request to update example #13 :slightly_smiling_face:

Iā€™ll pull if you want, but your solution could still be useful if the problem of persistence combined with query string cannot be solved.

I added to the repository a second example which adds query string capabilities to the first; if you append ?year=2100 to the URL of page 1, 2 or 3, you will see the year parameter being set accordingly.

Unfortunately it seems that there is some kind of bug in Dash persistence management (my original post was about this). In fact persistence works fine (as in first example) until you donā€™t use query strings; but after you apply a query string, as soon as you change page (and only the first time you change page after applying a query string) persistence is not able to keep the current value and initial value comes back, even if it should come back only on page refresh (persistence is of memory type).

From information printed on console by the line

print(year,hasattr(config.app_spanning_input, 'value'))

you may see that the code follows the same steps regardless of query string being applied just before page change, but the result is different, meaning that Dash persistence is unable to keep the value as expected under the conditions described above.

Should I report this as a bug?

Hi @nopria,

Yes, this looks like a bug, and there is an open pull request to fix something similar. I added your example to see if this could be addressed there too. Thanks for providing an excellent minimal example.

Store props from callback by nickmelnikov82 Ā· Pull Request #1900 Ā· plotly/dash Ā· GitHub.

Hello @nopria,

I dont know if I would classify this as a bug. It may be an undesired behavior.

Persistence is based upon user interaction, not the server determining what the users input will be. When you enter a query string, you are reloading the page and destroying the persistence. As seen if you watch the network tab.

My guess is that it is reseting to the initial value because you are manipulating the variable on the server.

Here, give this a test:

page2.py

import dash
from dash import dcc, html
from dash import Output, Input, State, callback

import config

dash.register_page(__name__, path="/page2")

def layout(year=None):
    print(year,hasattr(config.app_spanning_input, 'value')) # line for debug purposes
    if year is None and not config.app_spanning_input.value:
        config.app_spanning_input.value = config.app_spanning_input.initial
    elif type(year) is str: # a string value has been passed through a query string in URL
        config.app_spanning_input.value = int(year) if (all(c in '0123456789' for c in year) and int(year) in config.years) else None # make sure the query string value can be interpreted as an integer and it is in the options range
    return html.Div(
    [
        html.Label("Page 2 Select Year"),
        config.app_spanning_input,
        html.Div(id="page2-out"),
    ]
)


@callback(
    Output("page2-out", "children"),
    Input("spanning-pages-year", "value")
    )
def update(year):
    return f"The dropdown is {year}"

config.py

from dash import dcc

years = tuple(range(1000, 3000))

app_spanning_input = dcc.Dropdown(
    options=years,
    id="spanning-pages-year",
    persistence=True,
    persistence_type = 'memory',
)
app_spanning_input.initial = 2022
app_spanning_input.value = None

You were automatically overwriting the value if year wasnt present and altering itā€¦ but when you passed the year, you altered the backend variable, which forced it to refresh because it was technically different, though the id was the same, the element itself changed.

All I did was make an adjustment to make sure to only reset if there wasnt already a value declared.


Btw, I really like the spanning element bit, itā€™s top notch. I would have just brought it out into the main layout.

I tried your code and I think could be a workaround to obtain somewhat the expected behavior, in fact when refreshing the page initial value does not come back, as should be with persistence by memory.

What your code basically do is to add the condition

not config.app_spanning_input.value

that is always False except first time loading the page, allowing the initial value to be set only then. Unfortunately is False also when refreshing the page, and this prevents initial value to be set, leading to the incapability of restoring initial value on refresh.

I still think we are facing a bug, because query string usage should not change the persistence expected behavior as it does.

We should investigate about what exactly changes in Dash between first loading a page and refreshing, maybe we can spot exactly the bug or write a full working workaround for this case.

Iā€™m fairly certain that it has to pertain to the alterations being made to the value via code. This is in a sense altering the element itself, and breaks the persistence.

Can you write your query string to use a callback instead of inside the pageā€™s layout?

Also, the way that this works currently, multiple users would overwrite the value all the time.

I donā€™t think so, if you use the following code in config.py

def app_spanning_input(qsv):
    build_dict = dict(
        options=years,
        id="spanning-pages-year",
        persistence=True,
        persistence_type = 'memory',
    )
    if qsv is None:
        print('Setting initial value')
        build_dict['value'] = 2022 # set initial value if there is no query string
    else:
        print('Setting query string value')
        build_dict['value'] = int(qsv) if (all(c in '0123456789' for c in qsv) and int(qsv) in years) else None
    return dcc.Dropdown(**build_dict)

and the following code in pageX.py (where X is 1, 2 and 3)

def layout(year=None, **other_unknown_query_strings):
    return html.Div(
    [
        html.Label("Page X Select Year"),
        config.app_spanning_input(year),
        html.Div(id="pageX-out"),
    ]
)

there is no alteration of attribute by code (i.e. app_spanning_input.value=...) but the behavior is exactly the same as before, persistence lost (only) after the first page change after a query string refresh.

No, you are still changing it, to confirm, check this out, this involves no query string but alters the value upon each pass, there is no persistence because the elementā€™s value itself is being modified.

page1.py

import dash
from dash import dcc, html
from dash import Output, Input, State, callback

import config

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

def layout(year=None):
    print(year,hasattr(config.app_spanning_input, 'value')) # line for debug purposes
    config.app_spanning_input.value = 2021
    return html.Div(
    [
        html.Label("Page 1 Select Year"),
        config.app_spanning_input,
        html.Div(id="page1-out"),
    ]
)

@callback(
    Output("page1-out", "children"),
    Input("spanning-pages-year", "value")
    )
def update(year):
    return f"The dropdown is {year}"

page2.py

import dash
from dash import dcc, html
from dash import Output, Input, State, callback

import config

dash.register_page(__name__, path="/page2")

def layout(year=None):
    print(year,hasattr(config.app_spanning_input, 'value')) # line for debug purposes
    config.app_spanning_input.value = 2022
    return html.Div(
    [
        html.Label("Page 2 Select Year"),
        config.app_spanning_input,
        html.Div(id="page2-out"),
    ]
)


@callback(
    Output("page2-out", "children"),
    Input("spanning-pages-year", "value")
    )
def update(year):
    return f"The dropdown is {year}"

page3.py

import dash
from dash import dcc, html
from dash import Output, Input, State, callback

import config

dash.register_page(__name__, path="/page3")

def layout(year=None):
    print(year,hasattr(config.app_spanning_input, 'value')) # line for debug purposes
    config.app_spanning_input.value = 2023
    return html.Div(
    [
        html.Label("Page 3 Select Year"),
        config.app_spanning_input,
        html.Div(id="page3-out"),
    ]
)


@callback(
    Output("page3-out", "children"),
    Input("spanning-pages-year", "value")
    )
def update(year):
    return f"The dropdown is {year}"

Point being, you cant change the value systematically on the backend and expect persistence to stay in tack.

When you dont have a query, you are resetting the elementā€™s value to 2022, which resets the persistence.

Nowā€¦ the real question is why does this not work? (I placed ā€˜urlā€™ as a dcc.Location in the main app):

from dash import dcc, clientside_callback, Output, Input, State

years = tuple(range(1000, 3000))

app_spanning_input = dcc.Dropdown(
    options=years,
    id="spanning-pages-year",
    persistence=True,
    persistence_type = 'memory',
)
app_spanning_input.initial = 2022
app_spanning_input.value = 2022

clientside_callback(
    """
        function (p, v) {
            if (p) {
                if (p.includes('year=')) {
                    v = p.split('year=')[1]
                }
            }
            return v
        }
    """,
    Output('spanning-pages-year', 'value'),
    Input('url', 'search'),
    State('spanning-pages-year', 'value'),
)

You say ā€œthere is no persistence because the elementā€™s value itself is being modifiedā€, but persistence works fine even if the elementā€™s value itself is being modified, superseding the modified values as expected, except the first time you change page after a query string refresh. Thatā€™s why I donā€™t think that the problem is due to direct modification of element value.

Changing the value through user interface doesnā€™t alter the value on the backend, that is the difference between the two methods you are describing.

For clarification,

Query string alters the value systematically on the backend. This breaks persistence.

First time query string is not passed, it alters value systematically on the backend. This breaks persistence.

I agree that changing the value through user interface doesnā€™t alter the value on the backend, but switching page triggers the direct modification of value, and persistence still works fine.

Persistence breaks only the FIRST page change AFTER a query string. Thatā€™s why I donā€™t think the issue itā€™s related to direct modification of element value.

You are resetting the value to the initial value, which is the same as passing a year=2022 query string essentially. This overrides the persistence.

But Iā€™m resetting the value to the initial value also every time I switch page, and this does NOT override persistence. If you place some print commands to see what steps are being executed, youā€™ll understand what Iā€™m saying.