dash icon indicating copy to clipboard operation
dash copied to clipboard

[BUG] Dropdown: programatically set value isn't persisted

Open MM-Lehmann opened this issue 5 years ago • 12 comments

with dash==1.12.0 (haven't tested old versions) and persistence_type='session': setting a dcc.dropdown's value prop programatically, will update the GUI and callbacks alright, but the resulting persistence storage value (browser dev console) behaves very buggy. some observations:

  1. When setting the value for the first time (no persisted value yet), it would not appear in the storage list at all.
    • (re)setting it manually in the GUI comes up with a scrambled list containing both the manually set value (T) and the programatically set value (null): ["T",null]
    • the issue is only to be resolved when refreshing the page, deleting the storage entry is not sufficient
  2. When having set the value in the GUI first, and then repeating the process, the value disappears with the same result
  3. the persistence entry will keep the two items list until page refresh, even when clearing the dropdown: [null,null]
  4. the programatically set values are not recalled upon page refresh
  5. this goes for both multi=True and False

Expected behaviour: There should only be one item in the list at all times: [["T"]] or [[null]]

I can run more tests if required.

MM-Lehmann avatar May 15 '20 12:05 MM-Lehmann

scrambled list containing both the manually set value (T) and the programatically set value (null)

You're looking at what gets stored in window.sessionStorage? The back end for persistence does need to store both the programmatic and manual values, so it knows to bring back the manual value only if recreating the element with the same programmatic value.

So I wouldn't worry about the internals at this point, the question is whether there's a bug in the visible behavior. Can you give a concrete example of what you're seeing (app code, user action, result) that doesn't seem right?

alexcjohnson avatar May 15 '20 16:05 alexcjohnson

app.layout = html.Div([dcc.Dropdown(
    id='dropdown',
    multi=True,
    options=[{...}],
    persistence=True,
    persistence_type='session',
),
    dbc.Button("All", id="button"),
]),

@dash.callback(Output("dropdown", "value"),
               [Input("button", "n_clicks")],
               [State("dropdown", "options")])
def select_params(_all, params):
    return [par['value'] for par in params]

I've tried to extract the basic features of the dash control I'm having issues with.

user action: I manually select a few parameters in the dropdown, then I click the "all" button which fills the dropdown with all available options. result: upon page refresh, the values are not persisted and the dropdown stays empty. The same happens even after manually modifying the selection again after hitting "All".

Only a page refresh resolves the issue (until I hit "All" again).

MM-Lehmann avatar May 18 '20 16:05 MM-Lehmann

One further issue which is even more serious: (I don't know if that only started occurring with dash=1.13 or before) When a value (e.g. a checklist) is set programatically, I can't seem to reset it via the UI component. The callback always reads True for a persisted value that looks like this in the dev-panel (chrome): [[],[1]]

MM-Lehmann avatar Jun 20 '20 18:06 MM-Lehmann

Another Observation (may or may not be related): When a previously selected value in a dropdown becomes invalid (not contained in the options anymore after update), it would disappear from the UI element but is still included in the callback "value". This is weird and causes many issues in my use case. Proposal: "value" should always reflect what the user sees in the UI and not some orphan, invalid state.

MM-Lehmann avatar Aug 18 '20 09:08 MM-Lehmann

Another Observation (may or may not be related): When a previously selected value in a dropdown becomes invalid (not contained in the options anymore after update), it would disappear from the UI element but is still included in the callback "value". This is weird and causes many issues in my use case. Proposal: "value" should always reflect what the user sees in the UI and not some orphan, invalid state.

We see the observation in this comment as well, see #1373. This particular observation is not related to the dcc.Dropdown component alone, but for persistence in general (one question raised in #1373 is if components already can implement a persistence check on the JavaScript/client side, or if there are some changes needed in the core persistence machinery first).

anders-kiaer avatar Aug 19 '20 09:08 anders-kiaer

More generally, Dash components lose their persistency if they are modified by callbacks instead of direct user clicking (see: https://community.plotly.com/t/losing-persistence-value-on-refresh-when-input-value-updated-using-callback/39813 ). It would be very beneficial to keep the persistency even with callbacks

JiZur avatar Nov 30 '20 09:11 JiZur

I am experiencing the same issue. Neither value nor options are persisted when the content are created dynamically.

ManInFez avatar Dec 10 '20 13:12 ManInFez

I'm experiencing the same issue, but with other types of components. Inputs and data-tables are also affected by this bug in the case when their contents filled via callbacks. Persistence saving only triggered after direct user input action.

alexveden avatar Sep 07 '21 09:09 alexveden

Persistence saving only triggered after direct user input action.

That's as intended: if a value is set via callback, then the thing to be persisted should be the user input that led to that value, and that callback-set value will flow from the persisted user input. But I'm curious what use case you had in mind for callback outputs to be persisted?

alexcjohnson avatar Sep 07 '21 20:09 alexcjohnson

@alexcjohnson

But I'm curious what use case you had in mind for callback outputs to be persisted?

This thing is mostly annoying in multi-tab environments, so all my below examples are about erasing tab components state/values after user switched between tabs.

Case 1: data-table is not persistent when filled by callback (e.g. after Load button press)

When we fill data-table by callback (say on button or interval timer) the table data get erased on tab switch.

In the example below, are two identical tables one has static data, and another one can be filled by callback:

def create_table(tid, n=3):
    return dash_table.DataTable(
            id=tid,
            columns=[
                {'name': 'Filter', 'id': 'name'},
            ],
            data=[{'name': f'test{i}'} for i in range(n)],
            filter_action='native',
            page_action='none',
            row_deletable=True,
            style_table={
                'margin': '10px',
                'width': '100%',
            },
            persistence=True,
            persisted_props=['data'],
            persistence_type='session',
    )

tab_layout = [
    dbc.Row(create_table('tab1-table', 2)),
        dbc.Row(create_table('tab1-table2',5)),
        dbc.Button('Fill data', id='fill-table')
]

@app.callback(
        Output('tab1-table2', 'data'),
        Input('fill-table', 'n_clicks'),
)
def set_text(n_clicks):
    if n_clicks is None:
        raise PreventUpdate()
    return [{'name': f'btn_filler{i}'} for i in range(5)]

dash_table

Case 2: Input is not persistent when filled by callback

Particularly this affects hidden inputs that used to store some temporary data, but text inputs work the same. For example, if you need to store some temporary state of the tab components, which is built using multiple component inputs or results of callbacks.

Maybe I'm using a wrong tool for this task and should store state in dcc.Store somehow. But it seems more natural to me reusing persisted components values, it sounds more elegant to me.


layout = [
    html.Div([
        # Query panel
        dbc.Row([
                dbc.Input(id='tab2-input', type='text', persistence=True),
                dbc.Button('Fill by callback', id='tab2-set-text'),
            ]
        ),
    ])
]

@app.callback(
        Output('tab2-input', 'value'),
        Input('tab2-set-text', 'n_clicks'),
)
def set_text(n_clicks):
    if n_clicks is None:
        raise PreventUpdate()
    return 'Filled by callback'

dash_input

Simple Calculator App

The result doesn't persist after tab switch. This becomes a bit more annoying when calculation takes minutes or so.


layout = [
    html.Div([
        # Query panel
        dbc.Row([
                html.Label('Calculate expression'),
                dcc.Input(id='tab2-expr', persistence=True),
                ]),
        dbc.Row([
                html.Label('Result'),
                dbc.Input(id='tab2-input', type='text', persistence=True),
            ]),
        dbc.Row([

                dbc.Button('Calculate', id='tab2-set-text'),
            ]
        ),
    ])
]

@app.callback(
        Output('tab2-input', 'value'),
        Input('tab2-set-text', 'n_clicks'),
        State('tab2-expr', 'value'),
)
def set_text(n_clicks, expression):
    if n_clicks is None or not expression:
        raise PreventUpdate()
    
    return str(eval(expression))

dash_calculator

alexveden avatar Sep 08 '21 06:09 alexveden

I'm bumping into the same problem

That's as intended: if a value is set via callback, then the thing to be persisted should be the user input that led to that value, and that callback-set value will flow from the persisted user input. But I'm curious what use case you had in mind for callback outputs to be persisted?

In my case the thing to be persisted is a data file upload. A callback loads the file to a textarea field, where it can be modified by hand. The contents of the textarea must be persisted in the front. The contents of the textarea are then used by another callback to populate a chart.

For data security reasons we can't store that data in the backend, so the only way to share data is to share the data files securely.

The current behavior:

  • If we enter the data manually, it's persisted in the front
  • If we upload a data file, the data is not persisted, even if we edit it manually afterwards.

The workaround would be to update the chart in the same callback as the data file is uploaded to the textarea. Since I have multiple textareas feeding into the same chart, each with its own upload button, the resulting callback code is truly atrocious and borderline incomprehensible.

rodelrod avatar Mar 08 '22 09:03 rodelrod

One unfortunately rather hacky way to get persistence in these cases is to use a combination of dcc.Store (cache the value to be persisted), enrich's MultiplexerTransform (allow two callbacks on same output), and a hidden button triggered upon loading of component (fetch stored value when refreshing page).

Here's an example:

from dash_extensions.enrich import (
    DashProxy, MultiplexerTransform, TriggerTransform,
    html, dcc, Input, Output, State, Trigger
)

app = DashProxy(__name__, transforms=[MultiplexerTransform(),
                                      TriggerTransform()])

app.layout = html.Div([
    dcc.Input(id="input", type="text", persistence=True),
    html.Button(id="change-text-button", children="Change Input Text"),
    html.Button(id="hidden-button", style={"display": "none"}),
    dcc.Store(id="store", storage_type="session")
])


@app.callback(
    Output("store", "data"),
    Input("input", "value")
)
def update_store_on_input(value):
    """Store input value in dcc.Store for later recovery"""
    return value


@app.callback(
    Output("input", "value"),
    Trigger("change-text-button", "n_clicks"),
    prevent_initial_call=True
)
def change_input_text():
    """Change input text on button click"""
    return "Ex Machina"


@app.callback(
    Output("input", "value"),
    Trigger("hidden-button", "n_clicks"),
    State("store", "data")
)
def fetch_stored_input_on_reload(value):
    """Reload input value from dcc.Store upon reload of page: hidden button
    triggers callback only upon loading of page"""
    return value


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

pwan2991 avatar Aug 27 '22 07:08 pwan2991

@pwan2991, You can also achieve that with a circular callback and the callback context. In that case you don't need to have the MultiplexerTransform in your application, and you don't need to trigger or work with hidden component to fill the component upon loading. This is the approach I use in my applications, and works without relying on extensions. A consequence of this, of course, is that you have to combine everything in one callback.

Though I find it very cumbersome to have to do this for all components in my application. So I am really hoping that this functionality gets integrated in dash at some point.

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

app = Dash(__name__)

app.layout = html.Div([
    dcc.Input(id="input", type="text", persistence=True),
    html.Button(id="change-text-button", children="Change Input Text"),
    dcc.Store(id="store", storage_type="session")
])


@callback(
    Output("store", "data"),
    Output("input", "value"),
    Input("store", "modified_timestamp"),
    Input("input", "value"),
    Input("change-text-button", "n_clicks"),
    State("store", "data")
)
def update_store_on_input(_, text_input, manual_text_reset, stored_text):
    """ Handle all logic related to the input field"""

    if ctx.triggered_id == 'input':
        # The user filled in a value in the input field
        new_text_to_store = text_input
    elif ctx.triggered_id == 'change-text-button' and manual_text_reset:
        # The user pressed the button
        new_text_to_store = 'Ex Machina'
    else:
        # Something else happened like page reload, so we just fetch the data
        # that is stored
        new_text_to_store = stored_text

    # Store the new data in the dcc.Store and fill the input field with the
    # new data
    return new_text_to_store, new_text_to_store


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

aGitForEveryone avatar Oct 13 '22 12:10 aGitForEveryone