Dropdowns and dynamic options in Dash 4.0.0

Hello, I am having issues with dash 4.0 when using dropdowns and their options.

I have an app where the number of dropdowns and their options are dynamic.
The options also use a sub-structure for their labels, such as dcc.Markdown() or html.Div() (this is needed to trigger the bug)

This worked happily in dash 3.4.0, but when updating to 4.0 it fails when the dropdowns are regenerated while adding a new option.
Error:

can’t access property “props”, layout is undefined
…getProps persistence.js:286
…recordUiEdit persistence.js:321
…updateProps index.js:29

It seems like something in the layout path is not quite happy with how the options labels are adjusted.

The bug is avoided if any of these change:

Using Dash 3.4.0
Disable Persistence
Swap the labels to simple strings
Don’t add a new option, instead update a current option with new info

I thought maybe the options being returned were formatted strangely, or mixing new dcc.Markdown() with
returned json representations of the Markdown could be an issue, but swapping them out for fresh options,
exactly as they were sent originally, didn’t help.

To reproduce:

  1. Run the app
  2. Set dropdown Input: 0 to any option.
  3. Modify the top dropdown from option_set_1 to option_set_2
  4. The error will occur, and the rest of the updates to the dropdowns will be unreliable.

Here is a minimal example, plus some of my attempts to fix the problem:

Running Dash 4.0.0 and Python 3.14.2


from dash import html, Dash, Output, Input, callback, dcc, ALL, State

import json
import dash._utils


def main_get_dropdowns_app(server=True):
    app = Dash(__name__, server=server)

    app.layout = html.Div(
        id='top-level-component',
        children=[
            dcc.Dropdown(['option_set_1', 'option_set_2'], 'option_set_1', 
                         id='dropdown_selector', persistence=True, persistence_type='local'),
            html.Div(id='dropdowns_container', children=[]),
        ]
    )

    @callback(
        Output('dropdowns_container', 'children'),
        Input('dropdown_selector', 'value'),
        State({"aio_id": "extensible_dropdown", "index": ALL}, 'options')
    )
    def swap_dropdowns(dropdown_selector_value, prev_options):
        dropdowns_to_output = []
        if dropdown_selector_value == 'option_set_1':
            number_of_dds = 2
        else:
            number_of_dds = 4
        # All the options lists are the same, so just grab the first one
        # If this is the first run and none exit, generate fresh options
        if prev_options:
            options = prev_options[0]
        else:
            options = get_dropdown_options(3)

        # Ingest previous options and add a new one, this is what I want to do. This fails
        options.append(create_dropdown_option(len(options)))

        # Modify a prev option into a new one, this does work, but isn't want I need to do.
        # options[0] = create_dropdown_option(len(options))

        # Add a fresh set of options, but with an extra appended. This fails too
        # options.append(get_dropdown_options(len(options) + 1))

        # Attempted fix by forcing options to all be json, did not work.
        # if prev_options:
        #     for option in options:
        #         if not isinstance(label := option.get('label', {}), dict):
        #             print(f'This option is likly a dash object, not dict: {option=}')
        #             option['label'] = json.loads(dash._utils.to_json(label))

        for i in range(number_of_dds):
            dropdowns_to_output.append(
                html.Div([
                    html.Div(f'Input: {i}'),
                    dcc.Dropdown(
                        options=options,

                        id={"aio_id":"extensible_dropdown","index":f"{i}"},

                        persistence=True,
                        persistence_type='local'
                    ),
                ])
            )

        return dropdowns_to_output

    return app

def create_dropdown_option(name):
    return {
        'value': f'value_{name}',
        'label': dcc.Markdown(f'label_{name}', mathjax=True),       # Fails
        # 'label': html.Div(f'label_{name}'),                         # Fails
        # 'label': f'label_{name}',                                   # works but with missing functionality
        'title': f'title_{name}'
    }

def get_dropdown_options(num_of_options: int):
    options = []
    for name in range(num_of_options):
        options.append(create_dropdown_option(name))
    return options


if __name__ == '__main__':
    main_get_dropdowns_app().run(debug=True)