5555)
5656from reflex .vars .object import ObjectVar
5757
58+ _global_event_handlers : dict [str , EventHandler ] = {}
59+
60+
61+ def register_event_handler (name : str , handler : EventHandler ) -> None :
62+ """Register a decentralized event handler.
63+
64+ Args:
65+ name: The name of the event handler.
66+ handler: The event handler.
67+ """
68+ _global_event_handlers [name ] = handler
69+
70+
71+ def get_event_handler (name : str ) -> EventHandler | None :
72+ """Get a decentralized event handler by name.
73+
74+ Args:
75+ name: The name of the event handler.
76+
77+ Returns:
78+ The event handler, or None if not found.
79+ """
80+ return _global_event_handlers .get (name )
81+
5882
5983@dataclasses .dataclass (
6084 init = True ,
@@ -178,7 +202,7 @@ class EventHandler(EventActionsMixin):
178202
179203 # The full name of the state class this event handler is attached to.
180204 # Empty string means this event handler is a server side event.
181- state_full_name : str = dataclasses .field (default = "" )
205+ state_full_name : str | None = dataclasses .field (default = "" )
182206
183207 @classmethod
184208 def __class_getitem__ (cls , args_spec : str ) -> Annotated :
@@ -261,6 +285,13 @@ def __call__(self, *args: Any, **kwargs: Any) -> EventSpec:
261285 ) from e
262286 payload = tuple (zip (fn_args , values , strict = False ))
263287
288+ # Check if this is a decentralized event handler
289+ if self .state_full_name is None :
290+ from reflex .utils import format
291+
292+ name = format .to_snake_case (self .fn .__qualname__ )
293+ register_event_handler (name , self )
294+
264295 # Return the event spec.
265296 return EventSpec (
266297 handler = self , args = payload , event_actions = self .event_actions .copy ()
@@ -1492,6 +1523,9 @@ def check_fn_match_arg_spec(
14921523 Raises:
14931524 EventFnArgMismatchError: Raised if the number of mandatory arguments do not match
14941525 """
1526+ if is_decentralized_event_handler (user_func ):
1527+ return
1528+
14951529 user_args = list (inspect .signature (user_func ).parameters )
14961530 # Drop the first argument if it's a bound method
14971531 if inspect .ismethod (user_func ) and user_func .__self__ is not None :
@@ -1518,6 +1552,61 @@ def check_fn_match_arg_spec(
15181552 )
15191553
15201554
1555+ DECENTRALIZED_EVENT_MARKER = "_rx_decentralized_event"
1556+
1557+
1558+ def is_decentralized_event_handler (fn : Callable ) -> bool :
1559+ """Check if a function is a decentralized event handler.
1560+
1561+ Args:
1562+ fn: The function to check.
1563+
1564+ Returns:
1565+ Whether the function is a decentralized event handler.
1566+ """
1567+ # Check if the function has been decorated with @rx.event
1568+ if not hasattr (fn , "__qualname__" ):
1569+ return False
1570+
1571+ # Check if the function has the decentralized event marker
1572+ return hasattr (fn , DECENTRALIZED_EVENT_MARKER )
1573+
1574+
1575+ def wrap_decentralized_handler (fn : Callable ) -> Callable :
1576+ """Wrap a decentralized event handler to be used with component events.
1577+
1578+ This creates a wrapper that doesn't require the state parameter when called
1579+ from a component event, but will pass the state when the event is processed.
1580+
1581+ Args:
1582+ fn: The decentralized event handler to wrap.
1583+
1584+ Returns:
1585+ A wrapped function that can be used with component events.
1586+ """
1587+
1588+ # Create a wrapper function that doesn't require the state parameter
1589+ def wrapper (* args , ** kwargs ):
1590+ # Get or create the event handler
1591+ from reflex .utils import format
1592+
1593+ name = format .to_snake_case (fn .__qualname__ )
1594+ handler = _global_event_handlers .get (name )
1595+ if handler is None :
1596+ handler = EventHandler (fn = fn , state_full_name = None )
1597+ register_event_handler (name , handler )
1598+
1599+ # Create an event spec with no arguments - the state will be provided
1600+ return EventSpec (handler = handler , args = ())
1601+
1602+ wrapper .__name__ = fn .__name__
1603+ wrapper .__qualname__ = fn .__qualname__
1604+ wrapper .__doc__ = fn .__doc__
1605+ wrapper .__module__ = fn .__module__
1606+
1607+ return wrapper
1608+
1609+
15211610def call_event_fn (
15221611 fn : Callable ,
15231612 arg_spec : ArgsSpec | Sequence [ArgsSpec ],
@@ -1543,6 +1632,11 @@ def call_event_fn(
15431632 from reflex .event import EventHandler , EventSpec
15441633 from reflex .utils .exceptions import EventHandlerValueError
15451634
1635+ # Check if this is a decentralized event handler
1636+ if is_decentralized_event_handler (fn ):
1637+ wrapped_fn = wrap_decentralized_handler (fn )
1638+ return call_event_fn (wrapped_fn , arg_spec , key = key )
1639+
15461640 # Check that fn signature matches arg_spec
15471641 check_fn_match_arg_spec (fn , arg_spec , key = key )
15481642
@@ -2066,7 +2160,19 @@ def wrapper(
20662160 setattr (func , BACKGROUND_TASK_MARKER , True )
20672161 if getattr (func , "__name__" , "" ).startswith ("_" ):
20682162 raise ValueError ("Event handlers cannot be private." )
2069- return func # pyright: ignore [reportReturnType]
2163+
2164+ # Check if this is a method (defined in a class) or a standalone function
2165+ if hasattr (func , "__qualname__" ) and "." in func .__qualname__ :
2166+ return func # pyright: ignore [reportReturnType]
2167+ else :
2168+ # This is a decentralized event handler
2169+ handler = EventHandler (fn = func , state_full_name = None )
2170+ if background :
2171+ setattr (handler , BACKGROUND_TASK_MARKER , True )
2172+ # Mark the function as a decentralized event handler
2173+ setattr (func , DECENTRALIZED_EVENT_MARKER , True )
2174+ # Return the original function so it can be called normally
2175+ return func # pyright: ignore [reportReturnType]
20702176
20712177 if func is not None :
20722178 return wrapper (func )
0 commit comments