Skip to content

Conversation

@BSd3v
Copy link
Contributor

@BSd3v BSd3v commented Jan 12, 2026

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
Collaborator

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

Copy link
Contributor

@T4rk1n T4rk1n left a comment

Choose a reason for hiding this comment

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

💃

@T4rk1n
Copy link
Contributor

T4rk1n commented Jan 13, 2026

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

@BSd3v
Copy link
Contributor Author

BSd3v commented Jan 13, 2026

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

@AnnMarieW
Copy link
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
Copy link
Contributor

T4rk1n commented Jan 15, 2026

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

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
Collaborator

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

@BSd3v
Copy link
Contributor Author

BSd3v commented Jan 21, 2026

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.

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

4 participants