First Check
Example Code
from nicegui.observables import ObservableList
items = ObservableList([{}], on_change=lambda: print('change'))
for _ in range(3):
items[:] = list(items) # prints "change" 3 times in total
items[0]['x'] = 1 # prints "change" 4 times instead of once
Description
ObservableCollection._observe registers the container's _handle_change on any item that is already an ObservableCollection (observables.py, lines 45-47), and no removal or replacement operation (remove, pop, del, clear, item assignment) ever deregisters it. Handlers therefore accumulate:
- Rebuilding a list in place (
lst[:] = [...]) wraps the incoming plain list in a temporary ObservableList whose __init__ re-observes every surviving item. The temporary list is discarded immediately, but it stays reachable via the bound _handle_change handlers it left behind — one extra handler per rebuild, forever.
- Plain re-assignment (
options['series'] = [...]) behaves the same way: the replacement list re-observes the surviving items, and the handlers pointing at the discarded old list remain registered.
- After N rebuilds, a single mutation of a surviving item fires N+1 change events instead of one (i.e. N+1×
element.update() when the collection backs an element's props), and none of the discarded containers can be garbage-collected.
This hits the common pattern of updating element options by rebuilding a list, e.g. chart.options['series'] = [s for s in chart.options['series'] if ...] on ui.highchart/ui.echart/ui.aggrid. A long-running app that does this repeatedly accumulates handlers and dead containers without bound. (The "Adding and removing series" demo added in #6138 originally used this pattern; it now uses list.remove as a workaround.)
A related wart with the same root cause: re-inserting an item into a collection it already belongs to registers a duplicate handler (its _parent already bubbles events), so subsequent changes fire twice. on_change only guards against the item's own handler, not against duplicates or the parent's.
Expected behavior: removing or replacing an item detaches it from the container, so a subsequent mutation of a surviving item fires exactly one change event and discarded containers can be collected.
A fix probably needs explicit deregistration bookkeeping: an item can belong to several containers (which is why the single _parent reference is complemented by the extra-handler mechanism), so removal operations would have to unregister exactly the handler belonging to the removing container. Since Props, storage, and bindings all build on observables, the change deserves careful tests.
NiceGUI Version
main (3.14.0.post7.dev0)
Python Version
3.11.7
Browser
Other
Operating System
macOS
Additional Context
Browser and operating system are irrelevant — the behavior is purely server-side and reproducible without running an app.
First Check
Example Code
Description
ObservableCollection._observeregisters the container's_handle_changeon any item that is already anObservableCollection(observables.py, lines 45-47), and no removal or replacement operation (remove,pop,del,clear, item assignment) ever deregisters it. Handlers therefore accumulate:lst[:] = [...]) wraps the incoming plain list in a temporaryObservableListwhose__init__re-observes every surviving item. The temporary list is discarded immediately, but it stays reachable via the bound_handle_changehandlers it left behind — one extra handler per rebuild, forever.options['series'] = [...]) behaves the same way: the replacement list re-observes the surviving items, and the handlers pointing at the discarded old list remain registered.element.update()when the collection backs an element's props), and none of the discarded containers can be garbage-collected.This hits the common pattern of updating element options by rebuilding a list, e.g.
chart.options['series'] = [s for s in chart.options['series'] if ...]onui.highchart/ui.echart/ui.aggrid. A long-running app that does this repeatedly accumulates handlers and dead containers without bound. (The "Adding and removing series" demo added in #6138 originally used this pattern; it now useslist.removeas a workaround.)A related wart with the same root cause: re-inserting an item into a collection it already belongs to registers a duplicate handler (its
_parentalready bubbles events), so subsequent changes fire twice.on_changeonly guards against the item's own handler, not against duplicates or the parent's.Expected behavior: removing or replacing an item detaches it from the container, so a subsequent mutation of a surviving item fires exactly one change event and discarded containers can be collected.
A fix probably needs explicit deregistration bookkeeping: an item can belong to several containers (which is why the single
_parentreference is complemented by the extra-handler mechanism), so removal operations would have to unregister exactly the handler belonging to the removing container. SinceProps,storage, and bindings all build on observables, the change deserves careful tests.NiceGUI Version
main (3.14.0.post7.dev0)
Python Version
3.11.7
Browser
Other
Operating System
macOS
Additional Context
Browser and operating system are irrelevant — the behavior is purely server-side and reproducible without running an app.