From ba6397b79bae7d1a8e14e13a1e4804600233636b Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Fri, 12 Jun 2026 14:20:55 +0530 Subject: [PATCH 1/3] fix(memo): preserve runtime typing for unannotated params --- .../src/reflex_base/components/memo.py | 50 ++++++++++++++++--- tests/units/components/test_memo.py | 17 +++++++ 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/components/memo.py b/packages/reflex-base/src/reflex_base/components/memo.py index c31e37dd9db..1cb1fdd625a 100644 --- a/packages/reflex-base/src/reflex_base/components/memo.py +++ b/packages/reflex-base/src/reflex_base/components/memo.py @@ -278,6 +278,9 @@ class MemoComponentDefinition(MemoDefinition): export_name: str _component: _LazyBody[Component] + _runtime_param_values: dict[str, Any] = dataclasses.field( + default_factory=dict, repr=False, compare=False + ) # For passthrough wrappers built by the auto-memoize plugin: the # ``Bare``-wrapped ``{children}`` placeholder used when rendering the memo # body. The ``component`` keeps its ORIGINAL children so compile-time @@ -724,7 +727,11 @@ def _rest_placeholder(name: str) -> RestProp: return RestProp(_js_expr=name, _var_type=dict[str, Any]) -def _var_placeholder(name: str, annotation: Any) -> Var: +def _var_placeholder( + name: str, + annotation: Any, + runtime_value: Any | None = None, +) -> Var: """Create a placeholder Var for a memo parameter. Args: @@ -734,6 +741,11 @@ def _var_placeholder(name: str, annotation: Any) -> Var: Returns: The placeholder Var. """ + if _annotation_inner_type(annotation) is Any and runtime_value is not None: + runtime_type = ( + runtime_value._var_type if isinstance(runtime_value, Var) else type(runtime_value) + ) + return Var(_js_expr=name, _var_type=runtime_type).guess_type() return Var(_js_expr=name, _var_type=_annotation_inner_type(annotation)).guess_type() @@ -1001,6 +1013,7 @@ def finalize( def _evaluate_memo_function( fn: Callable[..., Any], params: tuple[MemoParam, ...], + runtime_values: Mapping[str, Any] | None = None, ) -> Any: """Evaluate a memo function with placeholder vars. @@ -1015,7 +1028,14 @@ def _evaluate_memo_function( keyword_args = {} for param in params: - placeholder = param.make_placeholder() + if param.kind is MemoParamKind.VALUE: + placeholder = _var_placeholder( + param.placeholder_name, + param.annotation, + runtime_values.get(param.name) if runtime_values is not None else None, + ) + else: + placeholder = param.make_placeholder() if param.parameter_kind in ( inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD, @@ -1267,7 +1287,9 @@ def _build_args_function( def _evaluate_component_body( - fn: Callable[..., Any], params: tuple[MemoParam, ...] + fn: Callable[..., Any], + params: tuple[MemoParam, ...], + runtime_values: Mapping[str, Any] | None = None, ) -> Component: """Run a component memo's body and return its compiled component. @@ -1281,7 +1303,9 @@ def _evaluate_component_body( Raises: TypeError: If the body does not return a component. """ - body = _normalize_component_return(_evaluate_memo_function(fn, params)) + body = _normalize_component_return( + _evaluate_memo_function(fn, params, runtime_values) + ) if body is None: msg = ( f"Component-returning `@rx.memo` `{fn.__name__}` must return an " @@ -1325,12 +1349,16 @@ def _create_component_definition( TypeError: If the function does not return a component. """ params = _analyze_params(fn, for_component=True) + runtime_param_values: dict[str, Any] = {} return MemoComponentDefinition( fn=fn, python_name=fn.__name__, params=params, export_name=format.to_title_case(fn.__name__), - _component=_LazyBody.ready(_evaluate_component_body(fn, params)), + _component=_LazyBody( + lambda: _evaluate_component_body(fn, params, runtime_param_values) + ), + _runtime_param_values=runtime_param_values, ) @@ -1593,8 +1621,14 @@ def __call__(self, *children: Any, **props: Any) -> MemoComponent: # Reading ``component`` materializes the deferred body, so ``type(...)`` # reflects the real wrapped class rather than the placeholder. + definition._runtime_param_values.clear() + definition._runtime_param_values.update(explicit_values) + try: + component_type = type(definition.component) + finally: + definition._runtime_param_values.clear() return _get_memo_component_class( - definition.export_name, type(definition.component) + definition.export_name, component_type )._create( children=list(children), memo_definition=definition, @@ -1881,15 +1915,17 @@ def memo(fn: Callable[..., Any]) -> _MemoComponentWrapper | _MemoFunctionWrapper # where the name resolves to ``wrapper`` (already bound by first use). definition: MemoComponentDefinition | MemoFunctionDefinition if is_component: + runtime_param_values: dict[str, Any] = {} definition = MemoComponentDefinition( fn=fn, python_name=fn.__name__, params=params, export_name=format.to_title_case(fn.__name__), _component=_LazyBody( - lambda: _evaluate_component_body(fn, params), + lambda: _evaluate_component_body(fn, params, runtime_param_values), placeholder=Fragment.create(), ), + _runtime_param_values=runtime_param_values, ) wrapper = _create_component_wrapper(definition) else: diff --git a/tests/units/components/test_memo.py b/tests/units/components/test_memo.py index 9276bf248c2..af83356a3a7 100644 --- a/tests/units/components/test_memo.py +++ b/tests/units/components/test_memo.py @@ -520,6 +520,23 @@ def soft_missing(value) -> rx.Component: assert "`value`" in kwargs["reason"] +def test_memo_uses_first_call_value_type_for_missing_param_annotation(): + """Component memos should infer missing parameter types from the first call.""" + + @rx.memo + def user_card(user) -> rx.Component: + return rx.box( + rx.heading(user["name"]), + rx.text(user["email"]), + ) + + component = user_card( + user={"name": "Ada", "email": "ada@example.com"}, + ) + + assert isinstance(component, MemoComponent) + + def test_memo_warns_on_missing_return_annotation(): """A missing return annotation should default to ``rx.Component`` with a warning.""" with patch.object(console, "deprecate") as mock_deprecate: From 4690ee555901a45ef56ee17e3aa82ccaca100f83 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Fri, 12 Jun 2026 20:28:15 +0530 Subject: [PATCH 2/3] fix memo var regression --- tests/units/components/test_memo.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/units/components/test_memo.py b/tests/units/components/test_memo.py index af83356a3a7..262c2e5107b 100644 --- a/tests/units/components/test_memo.py +++ b/tests/units/components/test_memo.py @@ -537,6 +537,23 @@ def user_card(user) -> rx.Component: assert isinstance(component, MemoComponent) +def test_memo_uses_var_runtime_value_type_for_missing_param_annotation(): + """Component memos should infer missing parameter types from runtime Vars.""" + + @rx.memo + def user_card(user) -> rx.Component: + return rx.box( + rx.heading(user["name"]), + rx.text(user["email"]), + ) + + component = user_card( + user=Var.create({"name": "Ada", "email": "ada@example.com"}), + ) + + assert isinstance(component, MemoComponent) + + def test_memo_warns_on_missing_return_annotation(): """A missing return annotation should default to ``rx.Component`` with a warning.""" with patch.object(console, "deprecate") as mock_deprecate: From dd3b4c83987916b74981fe27922e01f226f43719 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 13 Jun 2026 09:10:33 +0530 Subject: [PATCH 3/3] add runtime var type assertion --- tests/units/components/test_memo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/units/components/test_memo.py b/tests/units/components/test_memo.py index 262c2e5107b..60e089e0040 100644 --- a/tests/units/components/test_memo.py +++ b/tests/units/components/test_memo.py @@ -552,6 +552,8 @@ def user_card(user) -> rx.Component: ) assert isinstance(component, MemoComponent) + assert isinstance(component.user, Var) + assert component.user._var_type is dict def test_memo_warns_on_missing_return_annotation():