diff --git a/reflex/__init__.py b/reflex/__init__.py index 2de76d909..c3ab12753 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -328,7 +328,10 @@ _MAPPING: dict = { ], "middleware": ["middleware", "Middleware"], "model": ["session", "Model"], - "state": ["var"], + "state": [ + "var", + "dynamic", + ], "style": ["Style", "toggle_color_mode"], "utils.imports": ["ImportVar"], "utils.serializers": ["serializer"], diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index 2a8fcb452..2efa7c47f 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -184,6 +184,7 @@ from .middleware import middleware as middleware from .model import Model as Model from .model import session as session from .page import page as page +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 fc7b07e0d..9c264c5d6 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -30,6 +30,7 @@ from typing import ( Set, Tuple, Type, + TypeVar, Union, cast, get_args, @@ -78,6 +79,7 @@ from reflex.utils import console, format, path_ops, prerequisites, types from reflex.utils.exceptions import ( ComputedVarShadowsBaseVars, ComputedVarShadowsStateVar, + DynamicComponentInvalidSignature, DynamicRouteArgShadowsStateVar, EventHandlerShadowsBuiltInStateMethod, ImmutableStateError, @@ -94,6 +96,9 @@ from reflex.vars import VarData Delta = Dict[str, Any] var = computed_var +if TYPE_CHECKING: + from reflex.components.component import Component + # If the state is this large, it's considered a performance issue. TOO_LARGE_SERIALIZED_STATE = 100 * 1024 # 100kb @@ -2085,6 +2090,51 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): return state +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 StateProxy(wrapt.ObjectProxy): """Proxy of a state instance to control mutability of vars for a background task. diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index cd3d108b4..f16338513 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."""