Long callback progress update retriggers self

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!