This is a pretty specific question so I will do my best to break it down.
- I have a data table and I want to run an expensive task on each row
- As each task gets done, I want to update a cell in that row to signify completion
- A button press triggers the processing of the whole table
Here’s the complete code
import dash
from dash.dependencies import Input, Output, State
import diskcache
from dash.long_callback import DiskcacheLongCallbackManager
import time
cache = diskcache.Cache("./cache")
app = Dash(__name__, long_callback_manager=DiskcacheLongCallbackManager(cache))
table_data = [ {'x': 'x1', 'y': 'y1', 'status': '⬜'},
{'x': 'x2', 'y': 'y2', 'status': '⬜'},
{'x': 'x3', 'y': 'y3', 'status': '⬜'},
{'x': 'x4', 'y': 'y4', 'status': '⬜'}]
table = dash_table.DataTable(
data=table_data, id="my-table",
columns=[
{"id": "x", "name": "X"},
{"id": "y", "name": "Y"},
{"id": "status", "name": "Status"},
],
row_selectable='multi',
)
run_button = html.Button("Run", id="btn-run", className="button", n_clicks=0)
output_div = html.Div([], id="output-div")
@app.long_callback(
Output("output-div", "children"),
Input("btn-run", "n_clicks"),
State("my-table", "data"),
running=[ (Output("btn-run", "disabled"), True, False) ],
progress=[ Output("my-table", "data") ],
progress_default=[dash.no_update],
)
def clickRun(set_progress, clicks, my_table):
print("starting callback")
my_table=table_data
if clicks < 1:
return dash.no_update
for i,row in enumerate(my_table):
print(i)
time.sleep(1)
row["status"] = "✅"
set_progress([my_table])
time.sleep(1)
return ["done!"]
app.layout = html.Div(children=[ table, run_button, output_div ])
if __name__ == '__main__':
app.run_server(debug=True)
Here’s the relevant function
@app.long_callback(
Output("output-div", "children"),
Input("btn-run", "n_clicks"),
State("my-table", "data"),
running=[ (Output("btn-run", "disabled"), True, False) ],
progress=[ Output("my-table", "data") ],
progress_default=[dash.no_update],
)
def clickRun(set_progress, clicks, my_table):
print("starting callback")
if clicks < 1:
return dash.no_update
for i,row in enumerate(my_table):
print(i)
row["status"] = "✅"
set_progress([my_table])
time.sleep(2)
time.sleep(1)
return ["done!"]
What ends up happening is that this callback is getting retriggered every time a progress update alters the table.
The output ends up looking something like this:
starting callback
0
starting callback
0
1
starting callback
0
1
2
starting callback
0
1
2
3
This problem is solved by removing State("my-table", "data")
from the callback:
@app.long_callback(
Output("output-div", "children"),
Input("btn-run", "n_clicks"),
#State("my-table", "data"),
running=[ (Output("btn-run", "disabled"), True, False) ],
progress=[ Output("my-table", "data") ],
progress_default=[dash.no_update],
)
def clickRun(set_progress, clicks): #, my_table):
print("starting callback")
my_table=table_data
if clicks < 1:
return dash.no_update
for i,row in enumerate(my_table):
print(i)
row["status"] = "✅"
set_progress([my_table])
time.sleep(2)
time.sleep(1)
return ["done!"]
Output (correct):
starting callback
0
1
2
3
Here’s the question. My understanding is that including “State” in a callback means the callback is not triggered if the State property is updated. But in this situation, it seems that this behaviour is not being respected. Is this a bug? Am I missing something? Is there a way to work around this issue so I can update the rows of the table without re-triggering the long_callback?
Thanks!