Fix incorrect rendering in nested children
closes #3330
Note -this solution was provided by a Dash Enterprise customer. :tada:
In sample dash app below, a callback updates the children component with a layout of nested components. If there is a change in one of the nested components there can be unexpected results.
For example, if you select a date in the date picker, then "refresh" the layout in a callback, it re-renders the old state when you interact with the date picker again. This error happens in Dash 3 but not in Dash 2.
A workaround is to give the layout's parent component an id that changes with each update. This forces the component to re-render correctly.
This PR makes that workaround unnecessary. It updates the reducer in the dash renderer so it clears out the child path hashes and prevents stale values from triggering incorrect renders
Potential issues:
- This may impact performance when the children component has many nested components
- May cause issues when using partial property updates.
import dash
from dash import Input, Output, State, dcc, html
app = dash.Dash(__name__, prevent_initial_callbacks=True)
def layout(refresh_count=0):
return html.Div(
# id=f"container-{refresh_count}", # this works
id="container", # this doesn't work
children=[
html.H1(f"Layout refreshed {refresh_count} times"),
html.P(
html.Ul(
[
html.Li("Pick a date in the date picker"),
html.Li("Click on the 'Refresh layout' button"),
html.Li(
"Open date picker. It will change its value and text on the right will change too"
),
]
)
),
html.Div(
html.Div(
html.Div(
id="nested-container",
children=[
dcc.DatePickerSingle(id="date-picker", date=None),
html.Span(
f"Nothing picked, {refresh_count} refreshes",
id="text",
style={
"marginLeft": "10px",
"backgroundColor": "#d0d0d0",
"padding": "5px",
},
),
],
),
),
),
],
)
app.layout = html.Div(
[
html.H1("Dash bug showcase"),
html.Button("Refresh layout", id="refresh-button"),
html.P(
id="layout",
style={"padding": "20px", "border": "2px dashed red"},
children=layout(),
),
]
)
@app.callback(
Output("layout", "children"),
Input("refresh-button", "n_clicks"),
)
def update_output(n_clicks):
return layout(n_clicks)
@app.callback(
Output("text", "children"),
Input("date-picker", "date"),
State("refresh-button", "n_clicks"),
)
def update_text(date, n_clicks):
return f"{date}, {n_clicks} refreshes"
if __name__ == "__main__":
app.run(debug=True)
@T4rk1n @AnnMarieW I see performance risks:
This may impact performance when the children component has many nested components May cause issues when using partial property updates.
Can we mitigate these issues or does this need to be an optional rendering config?
Here's an interesting bit of data. The example above works fine in Dash 4 ( using the new dcc date picker), but does not work in Dash 3.
One more clue: In Dash 3, if the children are wrapped in a div instead of just being a list, it updates correctly:
children=html.Div([ # This line changed
dcc.DatePickerSingle(id="date-picker", date=None),
html.Span(
f"Nothing picked, {refresh_count} refreshes",
id="text",
style={
"marginLeft": "10px",
"backgroundColor": "#d0d0d0",
"padding": "5px",
},
),
]),