In dash >= 3.0.0 passing dash.no_update and still rerenders the full element

Hello everyone,

I have been working with dash < 3.0.0 and when upgrading encountered an unexpected behavior in a callback that can be triggered either by an interval or a button.

Before, if the callback was triggered by an interval and it returns a no_update to a DataTable property, it wouldn’t update the component. Dash wouldn’t re-render the element and therefore any edits/changes on the client side wouldn’t be lost even if the callback was triggered.

Now, with dash > 3.0.0, even when I pass a no_update in the callback, it re-renders the complete component, which makes any edit to be lost.

Please see the code below. With, dash > 3.0.0 you have 1 second interval to edit 'table-B' before the interval triggers the callback and the changes are lost.

Not really sure if this is the real purpose of no_update but thought it was worthwhile to ask the community.

PS. Actually, for DataTable in versions dash <3.0.0 I had to pass a no_update to component_property 'columns' and 'data'. If it is passed only to 'data' dash would re-render the whole DataTable

import numpy as np
import dash
from dash import dcc, Dash, Input, Output, callback, ctx
from dash.html import Br, Div, Button
from dash.dash_table import DataTable
app = Dash(__name__,)

data_A = [{"Column1": 0, "Column2": 0}]
data_B = [{'Column 1': 'B1', 'Column 2': 'B2'}, 
          {'Column 1': 'B3', 'Column 2': 'B4'}]

app.layout = Div([
    Div(children=[
        DataTable(id='table-A', columns=[{'name': 'Column1', 'id': 'Column1'},
                                         {'name': 'Column2', 'id': 'Column2'}],
                  data=data_A),
    ]),
    Br(),
    DataTable(id='table-B', columns=[{'name': 'Column 1', 'id': 'Column 1'},
                                     {'name': 'Column 2', 'id': 'Column 2'}],
              editable=True,
              data=data_B),
    Br(),
    Button('Update Both Tables', id='update-button'),              
    dcc.Interval(id='interval-component', interval=1000, n_intervals=0),
])

@callback(
    Output(component_id='table-B', component_property='columns'),
    Output(component_id='table-B', component_property='data'),
    Output(component_id='table-A', component_property='data'),
    Input(component_id='interval-component', component_property='n_intervals'),
    Input(component_id='update-button', component_property='n_clicks'),
    prevent_initial_call=True
)
def update_tables(n_intervals, n_clicks):
    button_id = ctx.triggered_id

    if button_id == 'update-button':
        data_A = [{"Column1": 0, "Column2": 0}]
        num = np.random.rand() * 10
        data_B = [{'Column 1': num, 'Column 2': num}, 
                  {'Column 1': num, 'Column 2': num}]
        return dash.no_update, data_B, data_A
    else:
        num = np.random.rand() * 10
        data_A = [{"Column1": num, "Column2": num * 10}]
        return dash.no_update, dash.no_update, data_A

if __name__ == "__main__":
    app.run(
        debug=True,
        port=8050,
        host='0.0.0.0',
    )