diff --git a/reflex/__init__.py b/reflex/__init__.py index ad51d2cf45..8ae54f4760 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -329,6 +329,7 @@ "SessionStorage", "ComponentState", "State", + "dynamic", ], "style": ["Style", "toggle_color_mode"], "utils.imports": ["ImportVar"], diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index d928778d82..2e87280279 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -184,6 +184,7 @@ from .state import Cookie as Cookie from .state import LocalStorage as LocalStorage from .state import SessionStorage as SessionStorage from .state import State as State +from .state import dynamic as dynamic from .state import var as var from .style import Style as Style from .style import toggle_color_mode as toggle_color_mode diff --git a/reflex/state.py b/reflex/state.py index 0634ad5b7c..6f620c5113 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -30,6 +30,7 @@ Set, Tuple, Type, + TypeVar, Union, cast, get_args, @@ -75,6 +76,7 @@ from reflex.utils.exceptions import ( ComputedVarShadowsBaseVars, ComputedVarShadowsStateVar, + DynamicComponentInvalidSignature, DynamicRouteArgShadowsStateVar, EventHandlerShadowsBuiltInStateMethod, ImmutableStateError, @@ -2091,6 +2093,51 @@ class State(BaseState): is_hydrated: bool = False +T = TypeVar("T", bound=BaseState) + + +def dynamic(func: Callable[[T], Component]): + """Create a dynamically generated components from a state class. + + Args: + func: The function to generate the component. + + Returns: + The dynamically generated component. + + Raises: + DynamicComponentInvalidSignature: If the function does not have exactly one parameter. + DynamicComponentInvalidSignature: If the function does not have a type hint for the state class. + """ + number_of_parameters = len(inspect.signature(func).parameters) + + func_signature = get_type_hints(func) + + if "return" in func_signature: + func_signature.pop("return") + + values = list(func_signature.values()) + + if number_of_parameters != 1: + raise DynamicComponentInvalidSignature( + "The function must have exactly one parameter, which is the state class." + ) + + if len(values) != 1: + raise DynamicComponentInvalidSignature( + "You must provide a type hint for the state class in the function." + ) + + state_class: Type[T] = values[0] + + def wrapper() -> Component: + from reflex.components.base.fragment import fragment + + return fragment(state_class._evaluate(lambda state: func(state))) + + return wrapper + + class FrontendEventExceptionState(State): """Substate for handling frontend exceptions.""" diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index cd3d108b4b..f163385138 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -139,3 +139,7 @@ class StateSchemaMismatchError(ReflexError, TypeError): class EnvironmentVarValueError(ReflexError, ValueError): """Raised when an environment variable is set to an invalid value.""" + + +class DynamicComponentInvalidSignature(ReflexError, TypeError): + """Raised when a dynamic component has an invalid signature."""