Skip to content

Commit bcc4b49

Browse files
add support for dynamic event handlers, streamline event handler formatting
1 parent ee4c7f1 commit bcc4b49

5 files changed

Lines changed: 169 additions & 20 deletions

File tree

reflex/compiler/templates.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from reflex import constants
1010
from reflex.constants import Hooks
1111
from reflex.constants.state import CAMEL_CASE_MEMO_MARKER
12-
from reflex.utils.format import format_state_name, json_dumps
12+
from reflex.utils.format import format_event_handler, format_state_name, json_dumps
1313
from reflex.vars.base import VarData
1414

1515
if TYPE_CHECKING:
@@ -284,8 +284,10 @@ def context_template(
284284

285285
# Compute dynamic state names that respect minification settings
286286
main_state_name = State.get_name()
287-
on_load_internal = f"{OnLoadInternalState.get_name()}.on_load_internal"
288-
update_vars_internal = f"{UpdateVarsInternalState.get_name()}.update_vars_internal"
287+
on_load_internal = format_event_handler(OnLoadInternalState.on_load_internal)
288+
update_vars_internal = format_event_handler(
289+
UpdateVarsInternalState.update_vars_internal
290+
)
289291
exception_state_full = FrontendEventExceptionState.get_full_name()
290292

291293
initial_state = initial_state or {}
@@ -314,15 +316,15 @@ def context_template(
314316
if (client_storage_vars && Object.keys(client_storage_vars).length !== 0) {{
315317
internal_events.push(
316318
ReflexEvent(
317-
'{state_name}.{update_vars_internal}',
319+
'{update_vars_internal}',
318320
{{vars: client_storage_vars}},
319321
),
320322
);
321323
}}
322324
323325
// `on_load_internal` triggers the correct on_load event(s) for the current page.
324326
// If the page does not define any on_load event, this will just set `is_hydrated = true`.
325-
internal_events.push(ReflexEvent('{state_name}.{on_load_internal}'));
327+
internal_events.push(ReflexEvent('{on_load_internal}'));
326328
327329
return internal_events;
328330
}}

reflex/constants/event.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from enum import Enum
44
from types import SimpleNamespace
55

6+
# The name of the setvar event handler.
7+
SETVAR = "setvar"
8+
69

710
class Endpoint(Enum):
811
"""Endpoints for the reflex backend API."""

reflex/state.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,7 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs):
609609
**cls.computed_vars,
610610
}
611611
cls.event_handlers = {}
612+
cls._event_id_to_name = {}
612613

613614
# Setup the base vars at the class level.
614615
for name, prop in cls.base_vars.items():
@@ -651,16 +652,9 @@ def __init_subclass__(cls, mixin: bool = False, **kwargs):
651652
cls.event_handlers[name] = handler
652653
setattr(cls, name, handler)
653654

654-
# Build event_id registry from minify.json configuration
655-
from reflex.minify import get_event_id, get_state_full_path, is_minify_enabled
656-
657-
cls._event_id_to_name = {}
658-
if is_minify_enabled():
659-
state_path = get_state_full_path(cls)
660-
for handler_name in events:
661-
event_id = get_event_id(state_path, handler_name)
662-
if event_id is not None:
663-
cls._event_id_to_name[event_id] = handler_name
655+
# Register user-defined event handlers for minification
656+
for handler_name in events:
657+
cls._register_event_handler_for_minify(handler_name)
664658

665659
# Initialize per-class var dependency tracking.
666660
cls._var_dependencies = {}
@@ -673,16 +667,42 @@ def _add_event_handler(
673667
cls,
674668
name: str,
675669
fn: Callable,
676-
):
670+
) -> EventHandler:
677671
"""Add an event handler dynamically to the state.
678672
679673
Args:
680674
name: The name of the event handler.
681675
fn: The function to call when the event is triggered.
676+
677+
Returns:
678+
The created EventHandler instance.
682679
"""
683680
handler = cls._create_event_handler(fn)
684681
cls.event_handlers[name] = handler
685682
setattr(cls, name, handler)
683+
cls._register_event_handler_for_minify(name)
684+
return handler
685+
686+
@classmethod
687+
def _register_event_handler_for_minify(cls, handler_name: str) -> None:
688+
"""Register an event handler for minification if applicable.
689+
690+
Called when an event handler is added to event_handlers dict.
691+
Updates _event_id_to_name if minification is enabled and the handler
692+
has a minified ID in the config.
693+
694+
Args:
695+
handler_name: The original name of the event handler.
696+
"""
697+
from reflex.minify import get_event_id, get_minify_config, get_state_full_path
698+
699+
if get_minify_config() is None:
700+
return
701+
702+
state_path = get_state_full_path(cls)
703+
event_id = get_event_id(state_path, handler_name)
704+
if event_id is not None:
705+
cls._event_id_to_name[event_id] = handler_name
686706

687707
@staticmethod
688708
def _copy_fn(fn: Callable) -> Callable:
@@ -1204,7 +1224,10 @@ def _create_event_handler(
12041224
@classmethod
12051225
def _create_setvar(cls):
12061226
"""Create the setvar method for the state."""
1207-
cls.setvar = cls.event_handlers["setvar"] = EventHandlerSetVar(state_cls=cls)
1227+
cls.setvar = cls.event_handlers[constants.event.SETVAR] = EventHandlerSetVar(
1228+
state_cls=cls
1229+
)
1230+
cls._register_event_handler_for_minify(constants.event.SETVAR)
12081231

12091232
@classmethod
12101233
def _create_setter(cls, name: str, prop: Var):
@@ -1244,6 +1267,7 @@ def __call__(self, *args, **kwargs):
12441267
)
12451268
cls.event_handlers[setter_name] = event_handler
12461269
setattr(cls, setter_name, event_handler)
1270+
cls._register_event_handler_for_minify(setter_name)
12471271

12481272
@classmethod
12491273
def _set_default_value(cls, name: str, prop: Var):

reflex/utils/format.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import json
77
import os
88
import re
9-
from typing import TYPE_CHECKING, Any
9+
from collections.abc import Callable
10+
from typing import TYPE_CHECKING, Any, cast
1011

1112
from reflex import constants
1213
from reflex.constants.state import FRONTEND_EVENT_STATE
@@ -439,7 +440,9 @@ def format_props(*single_props, **key_value_props) -> list[str]:
439440
] + [(f"...{LiteralVar.create(prop)!s}") for prop in single_props]
440441

441442

442-
def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]:
443+
def get_event_handler_parts(
444+
handler: EventHandler | Callable[..., Any],
445+
) -> tuple[str, str]:
443446
"""Get the state and function name of an event handler.
444447
445448
Args:
@@ -448,9 +451,13 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]:
448451
Returns:
449452
The state and function name (possibly minified based on minify.json).
450453
"""
454+
from reflex.event import EventHandler
451455
from reflex.minify import is_minify_enabled
452456
from reflex.state import State
453457

458+
# Cast for type checker - at runtime this is always an EventHandler
459+
handler = cast(EventHandler, handler)
460+
454461
# Get the class that defines the event handler.
455462
parts = handler.fn.__qualname__.split(".")
456463

@@ -486,7 +493,7 @@ def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]:
486493
return (state_full_name, name)
487494

488495

489-
def format_event_handler(handler: EventHandler) -> str:
496+
def format_event_handler(handler: EventHandler | Callable[..., Any]) -> str:
490497
"""Format an event handler.
491498
492499
Args:

tests/units/test_minification.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,116 @@ def my_handler(self):
419419

420420
# Should be the minified name directly
421421
assert event_name == "d"
422+
423+
424+
class TestDynamicHandlerMinification:
425+
"""Tests for dynamic event handler minification (setvar, auto-setters)."""
426+
427+
def test_setvar_registered_with_config(self, temp_minify_json):
428+
"""Test that setvar is registered in _event_id_to_name when config exists."""
429+
expected_module = "tests.units.test_minification"
430+
expected_state_path = f"{expected_module}.State.TestStateWithSetvar"
431+
432+
config: MinifyConfig = {
433+
"version": SCHEMA_VERSION,
434+
"states": {
435+
"reflex.state.State": "a",
436+
expected_state_path: "b",
437+
},
438+
"events": {
439+
expected_state_path: {"setvar": "s"},
440+
},
441+
}
442+
save_minify_config(config)
443+
clear_config_cache()
444+
State.get_name.cache_clear()
445+
State.get_full_name.cache_clear()
446+
State.get_class_substate.cache_clear()
447+
448+
class TestStateWithSetvar(State):
449+
pass
450+
451+
# Verify setvar is registered for minification
452+
assert "s" in TestStateWithSetvar._event_id_to_name
453+
assert TestStateWithSetvar._event_id_to_name["s"] == "setvar"
454+
455+
def test_auto_setter_registered_with_config(self, temp_minify_json):
456+
"""Test that auto-setters (set_*) are registered in _event_id_to_name when config exists."""
457+
expected_module = "tests.units.test_minification"
458+
expected_state_path = f"{expected_module}.State.TestStateWithAutoSetter"
459+
460+
config: MinifyConfig = {
461+
"version": SCHEMA_VERSION,
462+
"states": {
463+
"reflex.state.State": "a",
464+
expected_state_path: "b",
465+
},
466+
"events": {
467+
expected_state_path: {"set_count": "c", "setvar": "v"},
468+
},
469+
}
470+
save_minify_config(config)
471+
clear_config_cache()
472+
State.get_name.cache_clear()
473+
State.get_full_name.cache_clear()
474+
State.get_class_substate.cache_clear()
475+
476+
class TestStateWithAutoSetter(State):
477+
count: int = 0
478+
479+
# Verify auto-setter is registered for minification
480+
assert "c" in TestStateWithAutoSetter._event_id_to_name
481+
assert TestStateWithAutoSetter._event_id_to_name["c"] == "set_count"
482+
483+
def test_dynamic_handlers_not_registered_without_config(self, temp_minify_json):
484+
"""Test that dynamic handlers are NOT registered when no config exists."""
485+
# No config saved - temp_minify_json fixture ensures clean state
486+
487+
class TestStateNoConfig(State):
488+
count: int = 0
489+
490+
# Without config, _event_id_to_name should be empty
491+
assert TestStateNoConfig._event_id_to_name == {}
492+
493+
def test_add_event_handler_registered_with_config(self, temp_minify_json):
494+
"""Test that dynamically added event handlers via _add_event_handler are registered."""
495+
import reflex as rx
496+
497+
expected_module = "tests.units.test_minification"
498+
expected_state_path = f"{expected_module}.State.TestStateWithDynamicHandler"
499+
500+
config: MinifyConfig = {
501+
"version": SCHEMA_VERSION,
502+
"states": {
503+
"reflex.state.State": "a",
504+
expected_state_path: "b",
505+
},
506+
"events": {
507+
expected_state_path: {"dynamic_handler": "d", "setvar": "v"},
508+
},
509+
}
510+
save_minify_config(config)
511+
clear_config_cache()
512+
State.get_name.cache_clear()
513+
State.get_full_name.cache_clear()
514+
State.get_class_substate.cache_clear()
515+
516+
class TestStateWithDynamicHandler(State):
517+
pass
518+
519+
# Dynamically add an event handler after class creation
520+
@rx.event
521+
def dynamic_handler(self):
522+
pass
523+
524+
from reflex.event import EventHandler
525+
526+
handler = EventHandler(
527+
fn=dynamic_handler,
528+
state_full_name=TestStateWithDynamicHandler.get_full_name(),
529+
)
530+
TestStateWithDynamicHandler._add_event_handler("dynamic_handler", handler)
531+
532+
# Verify dynamic handler is registered for minification
533+
assert "d" in TestStateWithDynamicHandler._event_id_to_name
534+
assert TestStateWithDynamicHandler._event_id_to_name["d"] == "dynamic_handler"

0 commit comments

Comments
 (0)