dash icon indicating copy to clipboard operation
dash copied to clipboard

[BUG] Returning dash.no_update in overlapping callback calls overwrites most recently set value

Open martinwellman opened this issue 3 years ago • 3 comments

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.

martinwellman avatar Dec 09 '21 21:12 martinwellman