Skip to content

addEvents should flatten nested event lists instead of breaking the event queue #7319

Description

@masenf

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)
  1. Click Toggle sidebar: the flat event list works.
  2. Press Cmd+B on macOS or Ctrl+B elsewhere.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions