Every button.clicked() call registers a click listener that is never removed
Description
Each Button.clicked() call runs self.on('click', event.set, []), and Element has no way to remove a listener — _event_listeners is only cleared when the element is deleted. For a long-lived button that is awaited repeatedly (the pattern in the "Await button click" demo and any while True: await button.clicked() loop), this accumulates measurably:
- after N awaits, one physical click sends N websocket messages and triggers N
_handle_eventdispatches (verified: 4 listeners and 4 dispatches after 3 completed awaits), - every
clicked()whose registration lands after the initial render triggers"Event listeners changed after initial definition. Re-rendering affected elements."in the browser console and a full destroy-and-recreate of the button's DOM element — the flicker mechanism reported in #6248, _event_listenersand the serializedeventslist grow without bound.
The behavior is unchanged since clicked() was introduced (e9c23a60c, 2023).
Note that the console warning does not mitigate this: it is aimed at user code registering listeners after the first render (#5439, #6248), but here it is triggered by clicked()'s own internal registration — the documented demo produces it without the user calling on() at all, there is no way to avoid it while using clicked(), and the silent part of the problem (the unbounded accumulation and the N-fold dispatch per click) is not warned about at all.
Minimal example — open the page and click the button a few times: the label counts one additional click listener per completed await, and from the second click on the browser console logs the re-render warning for every click while the button's DOM element is destroyed and re-created.
from nicegui import ui
@ui.page('/')
async def page():
button = ui.button('Click me')
ui.label().bind_text_from(button, '_event_listeners',
backward=lambda listeners: f'{len(listeners)} click listeners')
while True:
await button.clicked()
ui.run()Suggested fix
Register a single click listener lazily on first use and let it wake a set of pending waiters. The CancelableWaitElement mixin from #6271 already maintains exactly this waiter registry, so the listener bookkeeping is the only missing piece; steady-state clicks then cause no updates and no re-renders at all.
Source: zauberzeug/nicegui