[BUG] Returning dash.no_update in overlapping callback calls overwrites most recently set value
Describe your context Please provide us your environment, so we can easily reproduce the issue.
dash 2.0.0
dash-bootstrap-components 1.0.0
dash-core-components 2.0.0
dash-daq 0.5.0
dash-google-oauth 1.2
dash-html-components 2.0.0
dash-renderer 1.4.1
dash-table 5.0.0
dash-uploader 0.6.0
jupyter-dash 0.4.0
-
if frontend related, tell us your Browser, Version and OS
- MacOS Monterrey 12.0.1
- Safari 15.1, Chrome 96.0.4664.93
Describe the bug
This bug is easier to reproduce when the app is running on a slow network, to simulate it I've added a time.sleep(2) call to the Python code.
A callback is called first, and then the same callback is called a second time before the first call has completed. The first call returns first, returning a value (eg. "1") to populate a Div's children with. The second call returns second, but this time returns dash.no_update (or raises PreventUpdate) to indicate to not change the Div's children.
Because dash.no_update was returned from the second call, the Div's child should be "1" (being set from the first call). Instead, the Div's child stays blank. This is because the second call (returning dash.no_update) restores whatever value the child was when the second call began (rather than leaving it unchanged).
To reproduce, run the app below. Click the button "Test 1" first, then immediately press "Test 2" after (within 2 seconds, before the call to time.sleep(2) finishes from "Test 1").
import dash
from dash import html
from dash.dependencies import Input, Output
import time
app = dash.Dash(__name__)
@app.callback(
Output("contents", "children"),
Input("test1", "n_clicks"),
Input("test2", "n_clicks"),
prevent_initial_call=True
)
def test(n_clicks1, n_clicks2):
# Get the Id that triggered the callback, and the corresponding # of clicks
trig_id = dash.callback_context.triggered[0]["prop_id"].split(".")[0]
trig_clicks = dash.callback_context.triggered[0]["value"]
print(f"Entering {trig_id}: {trig_clicks}")
time.sleep(2)
if trig_id == "test2":
print(f"Exiting {trig_id}: {trig_clicks} => dash.no_update")
return dash.no_update
print(f"Exiting {trig_id}: {trig_clicks} => {n_clicks1}")
return str(n_clicks1)
app.layout = html.Div(children=[
html.Button(id="test1", children="Test 1"),
html.Button(id="test2", children="Test 2"),
html.Div(id="contents")
])
if __name__ == "__main__":
app.run_server(debug=True)
Expected behavior
Expected behavior was described above. Returning dash.no_update should not change the contents of the Div from the most recent set. Instead it restores the value that the Div was when the callback was first initiated (rather than restoring the value when the callback ended)
The console output from clicking "Test 1" then "Test 2" quickly afterwards is shown below (where => indicates the return value):
Entering test1: 1
Entering test2: 1
Exiting test1: 1 => 1
Exiting test2: 1 => dash.no_update
The Div should have the value indicated by the line "Exiting test1" (ie. "1"), instead it is blank.
Very interesting, thanks for the clear example @martinwellman!
To be precise, what's happening is that the first callback return is ignored, because we see that another invocation of the same callback has already begun. So then when the second invocation returns, the dash.no_update is in reference to the initial state, not the state after the first invocation.
I think you're mostly correct about the end result we want, but I'm not sure it's always the right answer. The reason is potential State arguments. When the second invocation was called, these args had the values that existed on the page at that time, meaning that if any of them were going to change as a result of the first invocation we wouldn't capture those values in the second invocation.
So for example say you had two buttons, "Turn On" and "Turn Off"; an output that could say either "On" or "Off"; and that output was used as a State to the callback. The output starts out "Off" and you click "Turn On" and then "Turn Off" quickly. The first click sees the state "Off" so it returns "On". The second click also sees the state "Off" so it returns no_update. In that case we indeed want the output to end up "Off", but if we registered both returns the result would be "On".
I'm not entirely sure what the right answer is here... we might need to say that even though State arguments can't trigger a callback, we need to allow them to block a callback - in which case the second invocation wouldn't trigger until the first had completed. Or this might be just a caveat that we need to document with State arguments.
I agree it's hard to decide what the correct result should be. For my use case I have a lot of interconnected components on the page that can change the values of other components. Since I can only have one callback for one particular Output(id, value) I made a single large callback to handle a large portion of the UI, for updating the UI based on the current state and user input, and checking the triggered list for determining what to update in the callback. Because of this I had quite a lot of dash.no_update return values to improve performance (also the main component I was having problems with was a text input component, I find returning the input value for a text component unchanged leads to a messy input box because communication back and forth from the server can't keep up if the user is typing quickly, whereas returning dash.no_update doesn't have this problem. Now that I think about it the problem with text input and returning the value unchanged seems similar to the problem we're talking about now).
I did end up fixing the problem by splitting up the single large callback into smaller callbacks with fewer inputs and outputs. I think the case I described in the original issue would be the desired "correct" output, and having State block concurrent callbacks would be a good way to deal with it. Either way I think documentation of the behavior would be an excellent thing to add.
My own project is facing the same issue. I agree with @martinwellman about the desired "correct" output. Are there any plans to address this issue? Thank you!