Skip to content

Fixes components to remount when passed from a parent object - #3570

Merged
T4rk1n merged 13 commits into
plotly:devfrom
BSd3v:fix-react-state-parent-render-2
Mar 24, 2026
Merged

Fixes components to remount when passed from a parent object#3570
T4rk1n merged 13 commits into
plotly:devfrom
BSd3v:fix-react-state-parent-render-2

Conversation

@BSd3v

@BSd3v BSd3v commented Jan 12, 2026

Copy link
Copy Markdown
Contributor

fixes #3330

fixes issue where components wouldnt remount when passed as a children prop
eg:

import dash_mantine_components as dmc
from dash import Dash, Input, Output, State, no_update, html
from dash_ag_grid import AgGrid
app = Dash()

test = [
    AgGrid(
        id="ag-grid",
        rowData=[
            {"number": 1, "text": "a"},
        ],
        columnDefs=[
            {"field": "number", "filter": "agNumberColumnFilter"},
            {"field": "text", "filter": "agTextColumnFilter"},
        ]
    )
]

app.layout = dmc.MantineProvider(
    [
        html.Div(
            test,
        id='data-entry'
        ),
        dmc.Button('reload layout', id='reload-btn'),
    ],
    id="mantine-provider",
    defaultColorScheme="auto",
)

@app.callback(
    Output('data-entry', 'children'),
    Input('reload-btn', 'n_clicks'),
)
def reload_data(n_clicks):
    if n_clicks:
        return test
    return no_update

if __name__ == "__main__":
    app.run(debug=True)

Running this in the current dash will result in the component keeping some state even though it should technically be unmounted and remounted.

@AnnMarieW

Copy link
Copy Markdown
Collaborator

I tried this and confirmed that it fixes the example app from #3497

@T4rk1n T4rk1n left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💃

@T4rk1n

T4rk1n commented Jan 13, 2026

Copy link
Copy Markdown
Contributor

The redraw test show one less redraw after a click that might be a regression.

@BSd3v

BSd3v commented Jan 13, 2026

Copy link
Copy Markdown
Contributor Author

Yeah, I wasn't sure how this would perform with specifically that instance.

@AnnMarieW

Copy link
Copy Markdown
Collaborator

Do you think some apps are relying on the current behavior of holding the state, and this might be considered a breaking change?

@T4rk1n

T4rk1n commented Jan 15, 2026

Copy link
Copy Markdown
Contributor

Do you think some apps are relying on the current behavior of holding the state, and this might be considered a breaking change?

Yes it might effect apps that relies on keeping state even if the content is updated but it's expected to be the same component (with same id), it would reset to what is given back, which might be the correct behavior since it's a new component from the callback. Kind of an undocumented behavior and it's not tested beside the redraw component.

@safroze-plotly

Copy link
Copy Markdown

The fix doesn't work for this app (shared by DE client):

Workaround is to pass id to the layout

import dash
from dash import Input, Output, dcc, html

app = dash.Dash(__name__, prevent_initial_callbacks=True)


def layout_1():
    return dcc.Tabs(
        # id="tabs",  # NOTE: adding id somehow fixes the issue, same for layout_2
        value="tab1",
        children=[
            dcc.Tab(
                html.H2("First step is to switch to the next tab"),
                value="tab1",
                label="Click on next tab =>",
            ),
            dcc.Tab(
                html.H2("Now click on 'Layout 2' or 'Layout 3' button"),
                value="tab2",
                label="Click on me!",
            ),
        ],
    )


def layout_2():
    return html.Div(
        [
            html.H2("Click anywhere inside the red rectangle to see the issue"),
            html.Button("Button", style={"backgroundColor": "green", "padding": "50px"}),
            html.P(
                "Explenation: this Div inside container is replaced with dcc.Tabs after clicking"
            ),
        ]
    )


def layout_3():
    return dcc.Tabs(
        value="tab1",
        children=[
            dcc.Tab(
                html.H2("After clicking on this text last tab will disappear"),
                value="tab1",
                label="Tab 1",
            ),
            # NOTE: commenting the below tabs and repeating the steps will get you an error
            dcc.Tab("1", label="Tab 2"),
            dcc.Tab("2", label="Tab 3"),
        ],
    )


app.layout = html.Div(
    [
        html.H1("Dash bug showcase"),
        html.Button("Layout 1", id="button-1"),
        html.Button("Layout 2", id="button-2", style={"marginLeft": "10px"}),
        html.Button("Layout 3", id="button-3", style={"marginLeft": "10px"}),
        html.P(
            layout_1(),
            id="layout",
            style={"padding": "20px", "border": "2px dashed red"},
        ),
    ]
)


@app.callback(
    Output("layout", "children"),
    Input("button-1", "n_clicks"),
    Input("button-2", "n_clicks"),
    Input("button-3", "n_clicks"),
)
def update_output(_1, _2, _3):
    if dash.ctx.triggered_id == "button-1":
        return layout_1()

    if dash.ctx.triggered_id == "button-2":
        return layout_2()

    if dash.ctx.triggered_id == "button-3":
        return layout_3()


if __name__ == "__main__":
    app.run(debug=True)

@AnnMarieW

Copy link
Copy Markdown
Collaborator

@BSd3v - thanks for the update!
How is your latest commit differ from #3497?

@BSd3v

BSd3v commented Jan 21, 2026

Copy link
Copy Markdown
Contributor Author

While it was correct in theory, it assumed that children was the only time that you would need to reset the hashes. This is not the case for other children like props.

Children should rerender and descendents as well when passed from a parent. This was happening, however when there was a change to the state it was resetting due to the redux state. This now resets all descendant hashes so that there isn't a conflict with the hashed props interfering.

The mentioned PR also would do nothing to the react state, so the component reacting to initial load would not be the same. Some issues with components having async data would clear values when they would leave the data empty on original render. AG Grid would also encounter this.

@AnnMarieW

AnnMarieW commented Feb 22, 2026

Copy link
Copy Markdown
Collaborator

This PR would also fix an issue reported on the forum: https://community.plotly.com/t/dropdowns-and-dynamic-options-in-dash-4-0-0/96416/5

There is a bug when updating dropdown options in a callback where the label is a component. I ran the sample app provided and the error does not happen when running with the build in this PR.

@BSd3v
BSd3v requested a review from camdecoster as a code owner February 24, 2026 10:08
@AnnMarieW

Copy link
Copy Markdown
Collaborator

@CNFeffery

Could you please try the build in this PR to see if this bug fix affects performance in fac components?

@AnnMarieW

Copy link
Copy Markdown
Collaborator

Note also that the app above in #3570 (comment) works for me in the latest build. @safroze-plotly would you like to verify?

@CNFeffery

CNFeffery commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

@CNFeffery

Could you please try the build in this PR to see if this bug fix affects performance in fac components?

@AnnMarieW Based on the latest version of the current pr, I did not find any performance issues in the fac component library.

@T4rk1n
T4rk1n requested a review from KoolADE85 as a code owner March 24, 2026 16:59
@T4rk1n
T4rk1n merged commit d2e5bbe into plotly:dev Mar 24, 2026
12 checks passed
hdkfzyhhh pushed a commit to hdkfzyhhh/plotly-dash that referenced this pull request Aug 31, 2026
Since plotly#3570, every component passed down from a parent whose children
were updated by a callback got a bumped render key, forcing React to
unmount and remount the entire subtree on every callback run - even
when the returned children were the same components with only new prop
values. This reset descendant state and made large subtree updates 3-4x
slower (plotly#3846).

The forced remount is now conditional: the render key is only bumped
when the component identity (namespace, type, id) at that path actually
changed. The same component reconciles in place as before 4.2.0, while
a different component at the same path still remounts, and stale
descendant layout hashes are still reset - keeping plotly#3330 fixed.

The one reverted semantic from plotly#3570 is that returning the same
component (same type and id) from a callback no longer resets its
internal state; return it with a different id to force a remount.

Fixes plotly#3846

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Disappearing and Swapping Content in Dash v3

6 participants