Describe the bug
A reusable helper returning a list of events works when passed directly to an event trigger, but using that same list as an rx.match branch can compile to addEvents([[event1, event2]], ...). The frontend treats the inner array as an individual event instead of dispatching its contents.
On Reflex 0.9.12 this throws before the malformed entry is removed from the shared event queue, so the requested action never runs and subsequent UI actions stop working. The unhandled-rejection reporter enqueues another event into the same failing queue, allowing the failure to repeat.
Please support nested event lists so a conditional branch can reuse a multi-event action as a unit, without manually distributing its events across separate conditions.
To reproduce
Use this as the app module of a Reflex project:
import reflex as rx
class State(rx.State):
sidebar_open: bool = True
plan_mode: bool = True
@rx.event
def exit_plan_mode(self):
self.plan_mode = False
@rx.event
def toggle_sidebar(self):
self.sidebar_open = not self.sidebar_open
def toggle_events():
return [
State.exit_plan_mode().stop_propagation.prevent_default,
State.toggle_sidebar(),
]
def index():
return rx.vstack(
rx.text(rx.cond(State.sidebar_open, "Sidebar open", "Sidebar closed")),
rx.button("Toggle sidebar", on_click=toggle_events()),
rx.window_event_listener(
on_key_down=lambda key, modifiers: rx.cond(
modifiers["ctrl_key"] | modifiers["meta_key"],
rx.match(
key,
("s", rx.noop()),
("b", toggle_events()),
rx.noop(),
),
rx.noop(),
),
),
)
app = rx.App()
app.add_page(index)
- Click Toggle sidebar: the flat event list works.
- Press Cmd+B on macOS or Ctrl+B elsewhere.
- The sidebar does not toggle. Clicking the button afterward also fails to dispatch its events.
The b branch effectively produces:
addEvents([
[
ReflexEvent("...exit_plan_mode", {}, {stopPropagation: true, preventDefault: true}),
ReflexEvent("...toggle_sidebar", {})
]
], [event], {});
The error on 0.9.12 is:
TypeError: Cannot read properties of undefined (reading 'startsWith')
isStateful() calls event.name.startsWith(...) on the array. In this version that check happens before event_queue.shift(), so the entry remains in the queue and every subsequent dispatch fails again.
Expected behavior
Nested event lists should have the same dispatch semantics as their flattened equivalent: [[A, B], C] should dispatch A, B, then C. Preserve the normal event-action behavior, including preventDefault and stopPropagation on events inside the nested list.
Normalization needs to happen before addEvents collects event actions and names, as well as before queue insertion. Flattening only in queueEvents would still miss nested events' action flags. Equivalent compiler-side normalization would also need to support the Python conditional-list expression above.
Regression coverage should execute the generated handler, check nested/mixed/empty lists and existing null filtering, verify action flags and ordering, and dispatch an ordinary event afterward to ensure the queue remains usable. Unsupported malformed inputs should not leave the shared queue permanently blocked.
Verification and versions
- Reflex / reflex-base: 0.9.12
- reflex-components-core: 0.9.10.post1
- Python: 3.14.0
- OS: macOS
I compiled the standalone listener above and executed it with the installed framework's actual isStateful, queueEvents, and processEvent functions in Node, using a connected socket stub and a recording dispatch stub. No events dispatched; the first shortcut left one entry queued, and the next ordinary event threw the same error and left two entries queued.
I also inspected upstream main at b7dd25a6d4e2b92e08a5660f0b2968e9064213be: addEvents and queueEvents still only filter nullish top-level entries, without flattening. Main now skips isStateful() when the socket is connected, so the exact queue-blocking sequence described above is verified on 0.9.12, rather than claimed as an end-to-end reproduction on main.
Workaround / related context
We worked around this by building a top-level event list with a separate condition for each event returned by the helper. That avoids the nested array, but makes sharing a multi-event action between a button and a conditional keyboard shortcut unnecessarily awkward.
Related: #6185 and #6204 concern event composition. This reproduction needs only ordinary EventSpec values; it does not mix in an arbitrary FunctionVar.
Describe the bug
A reusable helper returning a list of events works when passed directly to an event trigger, but using that same list as an
rx.matchbranch can compile toaddEvents([[event1, event2]], ...). The frontend treats the inner array as an individual event instead of dispatching its contents.On Reflex 0.9.12 this throws before the malformed entry is removed from the shared event queue, so the requested action never runs and subsequent UI actions stop working. The unhandled-rejection reporter enqueues another event into the same failing queue, allowing the failure to repeat.
Please support nested event lists so a conditional branch can reuse a multi-event action as a unit, without manually distributing its events across separate conditions.
To reproduce
Use this as the app module of a Reflex project:
The
bbranch effectively produces:The error on 0.9.12 is:
isStateful()callsevent.name.startsWith(...)on the array. In this version that check happens beforeevent_queue.shift(), so the entry remains in the queue and every subsequent dispatch fails again.Expected behavior
Nested event lists should have the same dispatch semantics as their flattened equivalent:
[[A, B], C]should dispatchA,B, thenC. Preserve the normal event-action behavior, includingpreventDefaultandstopPropagationon events inside the nested list.Normalization needs to happen before
addEventscollects event actions and names, as well as before queue insertion. Flattening only inqueueEventswould still miss nested events' action flags. Equivalent compiler-side normalization would also need to support the Python conditional-list expression above.Regression coverage should execute the generated handler, check nested/mixed/empty lists and existing null filtering, verify action flags and ordering, and dispatch an ordinary event afterward to ensure the queue remains usable. Unsupported malformed inputs should not leave the shared queue permanently blocked.
Verification and versions
I compiled the standalone listener above and executed it with the installed framework's actual
isStateful,queueEvents, andprocessEventfunctions in Node, using a connected socket stub and a recording dispatch stub. No events dispatched; the first shortcut left one entry queued, and the next ordinary event threw the same error and left two entries queued.I also inspected upstream main at
b7dd25a6d4e2b92e08a5660f0b2968e9064213be: addEvents and queueEvents still only filter nullish top-level entries, without flattening. Main now skipsisStateful()when the socket is connected, so the exact queue-blocking sequence described above is verified on 0.9.12, rather than claimed as an end-to-end reproduction on main.Workaround / related context
We worked around this by building a top-level event list with a separate condition for each event returned by the helper. That avoids the nested array, but makes sharing a multi-event action between a button and a conditional keyboard shortcut unnecessarily awkward.
Related: #6185 and #6204 concern event composition. This reproduction needs only ordinary
EventSpecvalues; it does not mix in an arbitraryFunctionVar.