Component values unexpectedly reverting without callbacks firing

Related

I have the following components:

session_picker_row = dbc.Row(
    [
        dbc.Col(
            dcc.Dropdown(
                options=list(range(CURRENT_SEASON, 2017, -1)),
                placeholder="Select a season",
                value=None,
                id="season",
            )
        ),
        dbc.Col(
            dcc.Dropdown(
                options=[],
                placeholder="Select a event",
                value=None,
                id="event",
            ),
        ),
        dbc.Col(
            dcc.Dropdown(
                options=[],
                placeholder="Select a session",
                value=None,
                id="session",
            ),
        ),
        dbc.Col(
            dcc.Dropdown(
                options=[
                    {"label": "Finishing order", "value": False},
                    {"label": "Teammate side-by-side", "value": True},
                ],
                value=False,
                clearable=False,
                id="teammate-comp",
            )
        ),
        dbc.Col(
            dbc.Button(
                children="Load Session / Reorder Drivers",
                n_clicks=0,
                disabled=True,
                color="success",
                id="load-session",
            )
        ),
    ],
)

They are linked by some callbacks:

@callback(
    Output("event", "options"),
    Output("event", "value"),
    Output("event-schedule", "data"),
    Input("season", "value"),
    prevent_initial_call=True,
)
def set_event_options(
    season: int | None,
) -> tuple[list[str], None, dict]:
    """Get the names of all events in the selected season."""
    if season is None:
        return [], None, None

    schedule = f.get_event_schedule(season, include_testing=False)

    if season == CURRENT_SEASON:
        # only include events for which we have processed data
        last_round = DF_DICT[CURRENT_SEASON]["R"]["RoundNumber"].max()
        schedule = schedule[schedule["RoundNumber"] <= last_round]

    return (
        list(schedule["EventName"]),
        None,
        schedule.set_index("EventName").to_dict(orient="index"),
    )


@callback(
    Output("session", "options"),
    Output("session", "value"),
    Input("event", "value"),
    State("event-schedule", "data"),
    prevent_initial_call=True,
)
def set_session_options(event: str | None, schedule: dict) -> tuple[list[dict], None]:
    """
    Return the sessions contained in an event.

    Event schedule is passed in as a dictionary with the event names as keys. The values map
    column labels to the corresponding entry.
    """
    if event is None:
        return [], None

    return [
        {"label": "Race", "value": "R"},
        {
            "label": "Sprint",
            "value": "S",
            "disabled": schedule[event]["EventFormat"] not in SPRINT_FORMATS,
        },
    ], None


@callback(
    Output("session-debug", "children"),
    Input("load-session", "n_clicks"),
)
def session_debug(n_clicks):
    return n_clicks


@callback(
    Output("load-session", "disabled"),
    Input("season", "value"),
    Input("event", "value"),
    Input("session", "value"),
    prevent_initial_call=True,
)
def enable_load_session(season: int | None, event: str | None, session: str | None) -> bool:
    """Toggles load session button on when the previous three fields are filled."""
    return not (season is not None and event is not None and session is not None)

In my other issue, I had noted that some pandas code occasionally cause the button’s n_clicks property to revert to a previous state. When this happens, I also observe session’s value property reverting to the previous state. I have checked the callbacks graph to make sure these changes are not results of callbacks.

Might this have something to do with the browser cache? How can I investigate further?

I was not looking in the right place.

Here is a minimal program that has the same behavior:

"""Dash app layout and callbacks."""

import dash_bootstrap_components as dbc
import fastf1 as f
from dash import Dash, Input, Output, callback, dcc

# setup
min_session = f.get_session(2024, 14, "R")
min_session.load(telemetry=False)
laps = min_session.laps
MIN_LAPS = laps[["Time", "PitInTime", "PitOutTime"]]


scatter_y_options = [
    {"label": "Lap Time", "value": "LapTime"},
]

scatter_y_dropdown = dcc.Dropdown(
    options=scatter_y_options,
    value="LapTime",
    clearable=False,
    id="scatter-y",
)

app = Dash(
    __name__,
    external_stylesheets=[dbc.themes.SANDSTONE],
)
app.layout = dbc.Container(
    [
        dbc.Button(
            children="Load Session / Reorder Drivers",
            n_clicks=0,
            color="success",
            id="load-session",
        ),
        dcc.Store(id="laps"),
        scatter_y_dropdown,
    ]
)


@callback(
    Output("laps", "data"),
    Input("load-session", "n_clicks"),
    prevent_initial_call=True,
)
def get_session_laps(
    _: int,  # ignores actual_value of n_clicks
) -> dict:
    """Save the laps of the selected session into browser cache."""
    df = MIN_LAPS
    df = df.drop(columns=["Time", "PitInTime", "PitOutTime"])
    return df.to_dict()


@callback(
    Output("scatter-y", "options"),
    Input("laps", "data"),
    prevent_initial_call=True,
)
def set_y_axis_dropdowns(data: dict) -> list[dict[str, str]]:
    """Update y axis options based on the columns in the laps dataframe."""
    gap_cols = filter(lambda x: x.startswith("Gap"), data.keys())  # should be empty
    gap_col_options = [{"label": col, "value": col} for col in gap_cols]
    return scatter_y_options.extend(gap_col_options)


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

In a separate github issue, someone has figured out that the problem is with the second callback. The dictionary extend method returns None