Skip to content

Fix EventChain to separately invoke addEvents for mixed event types #6185

@masenf

Description

@masenf

Background: Event Types in Reflex

There are three kinds of events that can be bound to a component's event trigger (e.g. on_click):

  1. EventHandler — a reference to an @rx.event decorated function defined in an rx.State subclass. When bound to a trigger, this generates JS code that adds the event (mapping any trigger arguments) to the frontend queue to be sent to the server over the websocket and processed on the backend.

  2. Special frontend events — built-in events like _call_script, _console_log, etc. These also generate JS code that adds the event to the frontend event queue, but they are handled in a special frontend path without going to the backend.

  3. EventChainVar — an escape hatch where a Var (representing some expression in the compiled JS) is passed directly as an event trigger. It gets executed inline on the frontend, bypassing all Reflex event loop machinery.

Types 1 and 2 can be "chained" by passing them together in a list to an event trigger prop. Type 3 is special and currently can only be passed by itself.

Problem

Currently, it's not possible to mix normal state event handlers with custom FunctionVar event handlers in an event chain:

import reflex as rx

class State(rx.State):
    @rx.event
    def do_a_thing(self):
        print("Doing a thing!")

log_after_timeout = rx.vars.FunctionStringVar.create(
    "(...args) => { setTimeout(() => console.log('Timeout reached!', args), 1000); }"
).to(rx.EventChain)

def index():
    return rx.button(
        "Do both",
        on_click=[
            State.do_a_thing,       # EventHandler — needs addEvents()
            log_after_timeout,       # FunctionVar — should be called directly
        ]
    )

This fails because LiteralEventChainVar.create() tries to pass everything through a single addEvents([ ... ], args, actions) call. FunctionVar items don't serialize as ReflexEvent(...) objects, so they can't go through addEvents.

The current workaround is rx.call_function(log_after_timeout), but this is a cludge — it still generates code that routes through addEvents.

Goal

Make EventChain generate a JS function body that handles each item appropriately: addEvents() for EventHandler/EventSpec items, and direct function calls for FunctionVar items. The generated code should be a single function that executes each step in sequence.

Currently there are type checks that require a Var to be an EventChainVar, but this should be relaxed to allow any FunctionVar to be used directly in the chain.

Example Expected Output

For the example above, the generated JS should look something like:

(_event) => {
    addEvents([ReflexEvent("state.do_a_thing", {})], _event, {});
    ((...args) => { setTimeout(() => console.log('Timeout reached!', args), 1000); })(_event);
}

Rather than trying to pack both into a single addEvents([...]) array.

Acceptance Criteria

  • LiteralEventChainVar.create() results in each EventHandler/EventSpec event having a separate addEvents() (EventChain.invocation) call and invokes FunctionVar/EventVar events each as direct function calls
  • The generated JS function body is a sequence of statements (not a single expression), using explicit_return or a block body
  • Mixed chains work: on_click=[State.handler, some_function_var, State.other_handler] produces 3 separate invocations (addEvents, direct call, addEvents)
  • Pure EventHandler/EventSpec chains (the common case) should produce separate addEvents() calls per event — this allows different events to have different actions in the same chain.
  • Pure FunctionVar chains produce direct calls without addEvents
  • Event actions (preventDefault, stopPropagation, etc) are applied correctly regardless of mix
  • Unit tests covering: pure EventSpec chain, pure FunctionVar chain, mixed chain, mixed chain with event actions

Key Files

File Purpose
reflex/event.py:2041-2113 LiteralEventChainVar.create() — this is the main code to modify. Currently builds a single invocation.call(events_array, args, actions)
reflex/event.py:449-555 EventChain.create() — constructs the EventChain from mixed input types
reflex/vars/function.py:348-403 ArgsFunctionOperation — used to build the JS arrow function. May need explicit_return=True or a block-body variant
reflex/vars/function.py:410-459 ArgsFunctionOperationBuilder — builder pattern variant, base of LiteralEventChainVar
reflex/constants/compiler.py CompileVars.ADD_EVENTS, Hooks.EVENTS, Imports.EVENTS
reflex/utils/format.py format_prop() — where EventChain is converted to a Var for rendering

Notes

  • EventChain.events is typed as Sequence[EventSpec | EventVar | EventCallback] — you'll need to distinguish which items are "addEvents-compatible" (EventSpec/EventHandler/EventVar) vs "direct call" (FunctionVar)
  • The ArgsFunctionOperation supports explicit_return=True which generates { return expr } blocks — you may need a multi-statement block body instead
  • Look at how rx.call_function currently wraps FunctionVar to understand the existing workaround

Metadata

Metadata

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