From 351419137da626d6bff551c9b1b54a9dcc7369ba Mon Sep 17 00:00:00 2001 From: Maarten Breddels Date: Fri, 12 Jun 2026 14:15:12 +0200 Subject: [PATCH 1/2] Send vue event names with the widget construction state use_event now merges the event name into the element's _events kwarg during the render phase, so the synced trait is part of the comm open message. ipyvue's on_event only syncs _events when the event set differs, so the one update message per widget per event disappears without any ipyvue or protocol change. When the element is memoized and the widget already exists, on_event falls back to syncing as before. Halves the websocket message count for vuetify-heavy apps: 2145 -> 1121 messages on a 1024-button page load. Co-Authored-By: Claude Fable 5 --- reacton/ipyvue.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/reacton/ipyvue.py b/reacton/ipyvue.py index 580c21e..4588bd9 100644 --- a/reacton/ipyvue.py +++ b/reacton/ipyvue.py @@ -3,7 +3,7 @@ import ipyvue import reacton as react -from reacton.core import get_render_context +from reacton.core import ComponentWidget, get_render_context def use_event(el: react.core.Element, event_and_modifiers, callback: Callable[[Any], Any]): @@ -11,6 +11,20 @@ def use_event(el: react.core.Element, event_and_modifiers, callback: Callable[[A callback_ref = react.use_ref(callback) callback_ref.current = callback + # Put the event name in the widget constructor arguments: the synced _events + # trait then goes along with the comm open message. The later on_event call + # (in the effect below) only updates _events when the event set differs, so + # this saves one update message per widget per event. When the element is + # reused from a previous render (memoized) and the widget already exists, + # on_event falls back to syncing _events itself. + if isinstance(el.component, ComponentWidget) and issubclass(el.component.widget, ipyvue.VueWidget): + events = el.kwargs.get("_events") + if events is None: + el.kwargs["_events"] = [event_and_modifiers] + elif event_and_modifiers not in events: + # do not mutate the list, it could be shared with a previous element + el.kwargs["_events"] = [*events, event_and_modifiers] + def add_event_handler(): vue_widget = cast(ipyvue.VueWidget, react.core.get_widget(el)) # we are basically copying the logic from reacton.core._event_handler_exception_wrapper From a233933a0402ee2f30510fbdba34ad56d274822f Mon Sep 17 00:00:00 2001 From: Maarten Breddels Date: Fri, 12 Jun 2026 14:39:12 +0200 Subject: [PATCH 2/2] Skip _events sync for vue event handlers during render context close When a render context closes, every widget in it is about to have its comm closed, so syncing the _events trait after removing each event handler only produces one pointless update message per widget (1024 messages when leaving the 1024-button benchmark page). Partial subtree removal keeps the old behavior: an element owned by a surviving parent must still get its handler detached and synced. Also adds tests pinning the use_event message behavior: _events rides along with the widget construction state, never re-syncs on force_update or close, and still syncs when the event set changes on a persisting widget. Co-Authored-By: Claude Fable 5 --- reacton/ipyvue.py | 5 +++ reacton/ipyvue_test.py | 90 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 reacton/ipyvue_test.py diff --git a/reacton/ipyvue.py b/reacton/ipyvue.py index 4588bd9..d805834 100644 --- a/reacton/ipyvue.py +++ b/reacton/ipyvue.py @@ -46,6 +46,11 @@ def handler(*args): vue_widget.on_event(event_and_modifiers, handler) def cleanup(): + if rc._closing: + # the whole tree is going away: removing the handler would sync + # the _events trait to the frontend (one message per widget) + # right before the comm is closed anyway + return vue_widget.on_event(event_and_modifiers, handler, remove=True) return cleanup diff --git a/reacton/ipyvue_test.py b/reacton/ipyvue_test.py new file mode 100644 index 0000000..f7ebbb2 --- /dev/null +++ b/reacton/ipyvue_test.py @@ -0,0 +1,90 @@ +import unittest.mock + +import ipyvuetify +import ipyvue + +import reacton as react + +from . import ipyvuetify as v +from .ipyvue import use_event + + +def test_use_event_no_sync_messages(): + """The _events trait should be set at construction and never re-assigned. + + Re-assigning it (by on_event during the effect, or on_event(remove=True) + during close) sends one widget update message per widget per event. + """ + on_click = unittest.mock.Mock() + + @react.component + def Test(): + btn = v.Btn(children=["click me"]) + use_event(btn, "click", on_click) + return btn + + box, rc = react.render(Test(), handle_error=False) + btn = rc.find(ipyvuetify.Btn).widget + + events_changes = unittest.mock.Mock() + btn.observe(events_changes, "_events") + + # the event name went along with the constructor arguments + assert btn._events == ["click"] + # and the handler works + btn.fire_event("click", {}) + on_click.assert_called_once() + + rc.force_update() + events_changes.assert_not_called() + + rc.close() + events_changes.assert_not_called() + + +def test_use_event_removed_on_rerender(): + """When the widget persists but the event changes, _events must sync.""" + on_event = unittest.mock.Mock() + set_event_name = None + + @react.component + def Test(): + nonlocal set_event_name + event_name, set_event_name = react.use_state("click") + btn = v.Btn(children=["click me"]) + use_event(btn, event_name, on_event) + return btn + + box, rc = react.render(Test(), handle_error=False) + btn = rc.find(ipyvuetify.Btn).widget + assert btn._events == ["click"] + assert set_event_name is not None + set_event_name("dblclick") + assert btn._events == ["dblclick"] + btn.fire_event("dblclick", {}) + on_event.assert_called_once() + rc.close() + + +def test_use_event_component_element(): + """use_event on a component element (not a widget element) keeps working.""" + on_click = unittest.mock.Mock() + + @react.component + def Button(): + return v.Btn(children=["click me"]) + + @react.component + def Test(): + btn = Button() + use_event(btn, "click", on_click) + return btn + + box, rc = react.render(Test(), handle_error=False) + btn = rc.find(ipyvuetify.Btn).widget + assert isinstance(btn, ipyvue.VueWidget) + # falls back to syncing _events from the effect + assert btn._events == ["click"] + btn.fire_event("click", {}) + on_click.assert_called_once() + rc.close()