Djo fix repeating alert toasts - #416
Open
Odin107 wants to merge 3 commits into
Open
Conversation
added 2 commits
August 14, 2026 09:44
Since Dash 4.2.0 every callback write to a container's children remounts the DOM and replays the toast entry animation, so the 1s interval rewrite made one alert look like ~6. The display callback now skips ticks and containers whose visible toasts are unchanged.
Writing a container's children remounts every toast in it under Dash 4.2+, so a toast replayed its slide-in whenever a neighbour appeared or expired. Each toast now keeps a fixed slot for its whole life and only changed slots are written.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Fixes ephemeral alert toasts appearing ~6 times for a single triggered alert. The alerts were never duplicated in
alert_store- a rendering regression in Dash ≥ 4.2.0 remounts the toast DOM nodes on every 1-second interval tick, replaying the CSS entry animation each time. Toasts are now rendered into fixed per-toast slots, and only slots whose contents actually changed are written.Changes Made
src/ssb_dash_framework/utils/alert_handler.py_TOAST_POSITIONS,_MAX_TOASTS_PER_POSITION(4), and_toast_slot_id()._ephemeral_toast_state(), which assigns each visible ephemeral alert to a fixed slot and returns both the slot contents and a JSON-serializable signature.AlertHandler.layout()now renders_MAX_TOASTS_PER_POSITIONslot divs inside each of the three toast containers, plus adcc.Store(id="alert_toast_signature")holding the previous tick's signature. The threealert-container-*ids are unchanged.display_ephemeral_alertsnow outputs onechildrenper slot (12) plus the signature store. It raisesPreventUpdatewhen no slot changed and returnsdash.no_updatefor individual unchanged slots.variantlocal inshow_modal_alerts, annotatedcurrent_filterandmake_alertparameters, documentedcurrent_filter, updated thedcc.Intervalcomment.tests/test_alert_handler.pyWhy These Changes Were Needed
Since Dash 4.2.0 (plotly/dash#3570, tracked as #3846), any callback write to a container's
childrenbumps an internalfreshRenderscounter used as the React key of every child in that container. A changed key forces React to unmount and remount the subtree, and a freshly inserted DOM node restarts its CSS animation.display_ephemeral_alertsfires on a 1-seconddcc.Intervaland rebuilt all toasts unconditionally - harmless under Dash 3, which reconciled identical children in place. With the default 5-second duration this produced the initial mount plus ~5 remounts, i.e. the ~6 observed "pops".This is purely client-side: all ~28 callbacks writing to
alert_storehave disjoint triggers,AlertHandleris instantiated exactly once, and the store provably holds a single copy. The AgGrid copy notification was immune becauseshowCopyNotification()inassets/dashAgGridFunctions.jsappends a raw DOM node outside Dash's renderer.Implementation Details
id/keyprops on the toasts do not help. The remount is driven by the ancestor wrapper's internal counter, not child identity - the only lever is to avoid writingchildren.no_updatewas not sufficient. With two toasts in one container, removing the expired one still rewrote that container and remounted the survivor, replaying its slide-in. Reproduced in the browser as a single toast popping three times (appear, sibling-dying, sibling-expiry). Isolation therefore has to be per toast, not per container.created_at), and only genuinely new toasts claim a free slot. If a toast could move slots, both the old and new slot would be written and it would remount.created_atis stringified in the signature because it round-trips through browser JSON, which does not reliably preserve the float/int distinction (a whole-number float returns as an int).min-width: 500pxtoasts mean more than 4 would not fit sensibly anyway.Inputand drives appearance, the dying flip, and expiry. Idle ticks are now a cheap 204 with no DOM work, so no idle-disable logic was added.make_alertpreviously computeddyingwitha.get("duration", 6)while removal used5, so default-duration toasts could never reach the dying state and never played their exit animation. The unified default of5means they now do (dying at 4.2 s).Code Changes
Slot assignment keeps a toast in the slot it already holds (
src/ssb_dash_framework/utils/alert_handler.py, ~line 133):Layout: one slot div per toast, container ids preserved (~line 216):
Callback outputs and the skip logic (~line 470):
Per-slot writes (~line 545):
The
dyingflag moved out ofmake_alertinto_ephemeral_toast_state, somake_alert(a, dying)receives it rather than recomputing it from a secondtime.time()call.Reviewer Notes
_MAX_TOASTS_PER_POSITION = 4is a hard cap. A 5th concurrent toast at the same position is silently not displayed (it still appears in the modal log). Shout if you'd prefer a higher cap or an explicit overflow indicator.assets/dashAgGridFunctions.jsstill appends intoalert-container-bottom-left, which is unchanged; its node now lands after the slot divs. Removing the per-second rewrite makes that foreign node less likely to be disturbed.AlertHandler.layout()insidedcc.Tabswould still see the Tabs subtree re-render on writes (Tabs is the only component declaringdashChildrenUpdate); the framework's ownmain_layoutmounts it in the sidebar.