Skip to content

feat: allow explicit only event handlers - #7033

Open
benedikt-bartscher wants to merge 20 commits into
reflex-dev:mainfrom
benedikt-bartscher:disable-implicit-event-handlers
Open

benedikt-bartscher wants to merge 20 commits into
reflex-dev:mainfrom
benedikt-bartscher:disable-implicit-event-handlers

Conversation

@benedikt-bartscher

@benedikt-bartscher benedikt-bartscher commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Closes #7032

Review in cubic

@greptile-apps

greptile-apps Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] No new actionable issue was found in the reviewed changes.

Summary

Adds state_explicit_event_handlers, which keeps undecorated public state methods as Python helpers.

  • User-defined states can keep plain methods separate from event handlers.

Reviews (24) · Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." · Reviewed by Greptile

Comment thread packages/reflex-base/src/reflex_base/config.py Outdated
@codspeed

codspeed Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 4.46%

2 warnings

⚠️ 11 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 147 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ test_route_argument_extraction 118.9 µs 113.8 µs +4.46%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing benedikt-bartscher:disable-implicit-event-handlers (b9c3e71) with main (656e8c7)

Open in CodSpeed

@benedikt-bartscher
benedikt-bartscher marked this pull request as ready for review September 2, 2026 09:56
@benedikt-bartscher
benedikt-bartscher requested review from a team and Alek99 as code owners September 2, 2026 09:56
Comment thread packages/reflex-base/src/reflex_base/config.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 8 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/config.py Outdated
Comment thread tests/units/test_state.py
Comment thread news/7033.feature.md
Comment thread packages/reflex-base/src/reflex_base/config.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/config.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/config.py
@benedikt-bartscher
benedikt-bartscher force-pushed the disable-implicit-event-handlers branch from b2a522f to 945a53f Compare September 2, 2026 11:58

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 8 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/config.py

@masenf masenf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great, this gives us a lever and a chicken switch for eventually defaulting to requiring the decorator for event handlers.

need to ensure that reflex-enterprise's auth plugin and other integration points can still work with this new structure, but it seems like it would at first glance.

Comment thread docs/events/events_overview.md
@benedikt-bartscher

Copy link
Copy Markdown
Contributor Author

@masenf i think this PR is important from a security perspective - can we get it in?

Comment thread packages/reflex-base/src/reflex_base/config.py
Comment thread tests/units/test_state.py

@FarhanAliRaza FarhanAliRaza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested with a small app that sets state_explicit_event_handlers=True. The state has decorated handlers (plain, background=True, stop_propagation=True, an on_load handler, an explicit setter), undecorated helper methods called from the handlers, a mixin with one decorated and one undecorated method, and two rx.ComponentState instances. A computed var lists the state's registered event handler names on the page.

Ran it with reflex run in dev mode and exercised every control in the browser. All decorated handlers, the background handler, stop propagation, the mixin handler, the setter and both component states work. No console errors.

The handler list rendered from the backend still contains the undecorated helpers. The compile process sees the flag, but the backend worker does not, because it imports the app module before any config is loaded. A worker-style import (uvicorn.importer.import_from_string) reproduces this without a browser. I verified a candidate fix in _get_state_flag locally: the worker-style import then registers only the decorated methods, and the state, event and config unit tests pass.

The PR unit tests pass as-is. See the inline comments for the requested changes.

…d workers honor state_explicit_event_handlers

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread tests/units/test_state.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/config.py
Comment thread reflex/utils/exec.py
Comment thread reflex/utils/exec.py
@FarhanAliRaza
FarhanAliRaza force-pushed the disable-implicit-event-handlers branch from 69f329d to d81a948 Compare September 16, 2026 21:47
Comment thread reflex/state.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread reflex/state.py Outdated
…in/disable-implicit-event-handlers

# Conflicts:
#	tests/units/test_state.py
#	tests/units/utils/test_exec.py
Comment thread reflex/state.py Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread reflex/state.py Outdated
Comment thread reflex/state.py Outdated
Move marking undecorated state methods out of _item_is_event_handler into
_mark_undecorated_methods, which __init_subclass__ runs once in explicit
mode for the state and its mixins.
)
if config is not None:
return getattr(config, name)
env_val = os.environ.get(Config._prefixes[0] + name.upper())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the env var is not set and no config is cached, the flag falls back to False with no warning. Every public method then becomes an event handler again. This happens for any entry point other than reflex run, for example uvicorn/gunicorn pointed at the app module, or a test suite that imports the state modules.

Repro with an app that sets state_explicit_event_handlers=True, and no REFLEX_STATE_EXPLICIT_EVENT_HANDLERS in the environment:

from uvicorn.importer import import_from_string
import_from_string("pr7033_repro.pr7033_repro:app")
sorted(ExplicitState.event_handlers)
# [..., 'mixin_helper', ..., 'step', ...]  <- undecorated helpers are handlers

The docs mention this. But for a security switch, a silent fallback to the permissive mode is the wrong failure. Please make it fail closed. For example: record the value the first user State was built with. When the config is loaded (e.g. in App.__init__), raise if the config value differs.

Comment thread reflex/utils/exec.py
if not frontend_present:
notify_backend(host)

get_config()._persist_state_flags()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exported env var also wins over later rxconfig.py edits, because update_from_env applies REFLEX_* over the file values. In dev, the worker reloads with the value from startup.

Repro: start reflex run with state_explicit_event_handlers=False, then set it to True and save. After the reload, step and mixin_helper are still registered handlers. Only a full restart applies the change. The reverse edit (True to False) is also ignored.

Please do not let the persisted value override the file, or document that a change to this flag needs a restart.

raise EventFnArgMismatchError(msg)


def _check_not_undecorated_state_method(value: Any, key: str | None) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check runs only when a trigger is compiled. The runtime paths through Event.from_event_type and fix_events do not call it. Both cases below work in implicit mode, so users who migrate will hit them:

  • app.add_page(page, on_load=State.undecorated) is accepted. Every load of the page then fails in the backend with ValueError: Unexpected event type, <class 'function'>. The browser shows no error, and on_load does not run.
  • An event handler that does return State.undecorated (or yields it) fails with TypeError: Your handler State.chain must only return/yield: None, Events....

Neither message mentions @rx.event or state_explicit_event_handlers. Please call this check on the runtime path too, so that on_load and return/yield values get the same targeted error. Please also add tests for both cases.

Comment thread reflex/state.py
and not isinstance(value, EventHandler)
and not getattr(value, "__override_base_method__", False)
and hasattr(value, "__code__")
and (not explicit or getattr(value, EVENT_MARKER, False))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A decorator without functools.wraps on top of @rx.event loses this marker. So the method silently stops being an event handler:

def require_login(f):
    def inner(self, *a, **k):
        return f(self, *a, **k)
    return inner

class S(rx.State):
    @require_login
    @rx.event
    def save(self): ...

rx.button(on_click=S.save)
EventFnArgMismatchError: Event on_click only provides 1 arguments, but <function require_login.<locals>.inner ...> requires at least 3 arguments to be passed to the event handler.

The wrapper's __qualname__ is require_login.<locals>.inner, so _mark_undecorated_methods also skips it, and the targeted error does not fire. The same code works on main. It also works with functools.wraps, or with @rx.event as the outer decorator.

Please give this case a clear error that names the flag. Or at least document that @rx.event must be the outermost decorator, or that wrappers must use functools.wraps.

raise ValueError(msg)
# Lets State tell decorated methods apart when
# state_explicit_event_handlers is enabled.
setattr(func, EVENT_MARKER, True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setattr runs in the default mode too. So rx.event(x) now raises for any x without a __dict__:

rx.event(Obj().m)
# AttributeError: 'method' object has no attribute '_rx_event' and no __dict__ for setting new attributes
rx.event(len)
# AttributeError: 'builtin_function_or_method' object has no attribute '_rx_event' ...

On main, both calls return the value unchanged. I did not find this usage in the repo or the docs, but it is a behavior change for users who do not enable the flag. Please guard the setattr, or give a clear error.

Comment on lines +3055 to +3056
EVENT_MARKER = EVENT_MARKER
UNDECORATED_STATE_METHOD_MARKER = UNDECORATED_STATE_METHOD_MARKER

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two markers are internal, but adding them to EventNamespace makes them public as rx.event.EVENT_MARKER and rx.event.UNDECORATED_STATE_METHOD_MARKER. reflex/state.py imports them from reflex_base.event directly, so the namespace entries are not needed. Please remove them, so that no new public API has to be supported.

@FarhanAliRaza FarhanAliRaza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-tested at b9137c6 with the same example app as before, with state_explicit_event_handlers=True. The app has decorated handlers (plain, background, stop_propagation, on_load, a setter), undecorated helpers, a mixin, and two ComponentState instances. I ran it with reflex run in dev mode and drove it in a headless browser.

Both earlier requests are fixed. The backend worker now registers only the decorated handlers. Wiring an undecorated method to a trigger gives a clear error that names @rx.event. I also sent a crafted websocket event for an undecorated method, and the backend rejected it. All the handlers work in the browser, with no console errors.

I also tried edge cases around the flag: on_load, handlers that return an undecorated method, stacked decorators, subclass overrides, decentralized handlers, and a worker-style import without the env var. The targeted state, event, config and exec unit tests pass. ruff and pyright are clean.

See the inline comments for the remaining points.

…ent-handlers

# Conflicts:
#	tests/units/test_state.py
#	tests/units/utils/test_exec.py
@masenf masenf added the documentation Improvements or additions to documentation label Oct 6, 2026
@masenf masenf added the roadmap https://github.com/reflex-dev/reflex/issues/2727 label Oct 23, 2026

This branch has not been deployed

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

Labels

documentation Improvements or additions to documentation roadmap https://github.com/reflex-dev/reflex/issues/2727

Projects

None yet

Development

Successfully merging this pull request may close these issues.

disable implicit @rx.event handlers

4 participants