-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
Problem
The current @rx.memo decorator (alias for custom_component) only supports returning rx.Component objects. It compiles decorated functions into standalone React components (exposed at .web/utils/components.js). There's a need for a new experimental variant that:
- Supports returning arbitrary
Vartypes (not just Components) — generating a JS function and exposing the decorated name as a typedFunctionVarthat imports the generated function definition. - Also supports returning
rx.Component— generating a React component (similar to existing@rx.memobut with the new argument conventions below) - Enforces that all arguments are annotated as
Var[...]types - Handles special argument patterns:
childrenand rest props
How It Works
The decorator inspects the function signature, creates Var placeholder objects for each parameter, calls the function at decoration time to produce a return expression, and wraps the result in an ArgsFunctionOperation (for Var returns) or a compiled React component (for Component returns).
Since this operates at compile time by calling the function with Var placeholders, *args and **kwargs are not supported — Python's variadic constructs can't be meaningfully replicated in this compile-time evaluation model. Instead, two special argument patterns are provided:
Special Argument Handling
Calling convention: The object returned by @rx._x.memo only accepts positional arguments that are rx.Component or rx.Var[rx.Component] (i.e. children). All other props must be passed as keyword arguments.
children: rx.Var[rx.Component] — If a parameter named children is present and typed as rx.Var[rx.Component], it will appear as children destructured from the props in the generated JS function signature, but the resulting function on the Python side will accept *children as positional arguments. Only rx.Component or rx.Var[rx.Component] values may be passed positionally.
rx.RestProp — A new type (based on ObjectVar) that represents remaining props. If a parameter is typed as rx.RestProp, it renders as ...rest in the generated JS function signature (using FunctionArgs.rest). When a component encounters the RestProp as a child, it should instead set its spread as a "special_prop" in the component. This allows the memo to forward arbitrary extra props.
Prior Art: LiteralLambdaVar from reflex-enterprise
The following pattern from reflex-enterprise shows the core idea:
# Simplified from reflex-enterprise's LiteralLambdaVar.create()
sig = inspect.signature(func)
hints = get_type_hints(func)
params = []
for param in sig.parameters.values():
annotation = hints.get(param.name, param.annotation)
# Enforce: all params must be Var[...] annotated
if not typehint_issubclass(annotation, Var):
raise TypeError(
f"All parameters of {func.__name__} must be annotated as rx.Var[...], got {annotation}"
)
params.append(Var(param.name, _var_type=annotation_args[0]))
return_expr = func(*params) # Call with Var placeholders to get JS expression
return ArgsFunctionOperation.create(
args_names=tuple(sig.parameters),
return_expr=Var.create(return_expr),
)Key validation from the enterprise version:
- All parameters must be
Var[T]annotated - The function must be evaluable at compile time using only Var operations
- Return expressions that use hooks or dynamic imports are rejected (with guidance to use
@rx.memocomponent instead) - Non-bundled library imports in the return expression are rejected
Proposed API
This should be implemented in the experimental namespace (rx._x.memo).
import reflex as rx
# --- Var-returning memo: generates a JS arrow function ---
@rx._x.memo
def format_price(amount: rx.Var[int], currency: rx.Var[str]) -> rx.Var[str]:
return currency + rx.Var.create(": $") + amount.to(str)
# format_price is now a FunctionVar that can be used in Var expressions:
rx.text(format_price(SomeState.price, SomeState.currency))
# --- Component-returning memo: generates a React.memo component ---
@rx._x.memo
def my_card(
children: rx.Var[rx.Component],
rest: rx.RestProp,
*,
title: rx.Var[str],
) -> rx.Component:
return rx.box(
rx.heading(title),
children,
rest,
)
# On the Python side, children are passed as positional args:
my_card(rx.text("child 1"), rx.text("child 2"), title="Hello", class_name="extra")
# --- Var-returning memo with rest props ---
@rx._x.memo
def merge_styles(
base: rx.Var[dict[str, str]],
overrides: rx.RestProp,
) -> rx.Var[dict[str, str]]:
return base.merge(overrides)Acceptance Criteria
- Implemented in the experimental namespace:
rx._x.memo(i.e.reflex/experimental/) - The
@rx._x.memodecorator detects the return type annotation to decide behavior:rx.Componentreturn → generate a React.memo componentVar[T]return → generate aFunctionVar/ArgsFunctionOperation
- All parameters must be annotated as
Var[T](or the special types below) — raiseTypeErrorotherwise -
*argsand**kwargsin the Python signature are rejected with a clear error - Calling convention: the object returned by the decorator only accepts positional arguments that are
rx.Componentorrx.Var[rx.Component](children). All other props must be passed as keyword arguments. -
childrenhandling: a parameter namedchildrentyped asrx.Var[rx.Component]is accepted and rendered as destructuredchildrenin the JS signature. If present, on the Python side, the decorated function accepts*childrenas positional arguments. -
RestProphandling: define a newrx.RestProptype (based onObjectVar). A parameter typed asRestProprenders as...restin the generated JS viaFunctionArgs.rest. If present, on the python side, the function accepts**restas arbitrary keyword arguments. - The function is called at decoration time with Var placeholders to produce the return expression
- The decorated name behaves as a
FunctionVarwith the correct return type (for Var returns) or as a component factory (for Component returns) - Unit tests covering: Var-returning memo, Component-returning memo,
childrenparameter,RestPropparameter, type enforcement on params, rejection of*args/**kwargs
Key Files
| File | Purpose |
|---|---|
reflex/components/component.py:2172-2216 |
Current custom_component() / memo — reference for existing behavior |
reflex/components/component.py:1912-2130 |
CustomComponent class — existing component memo path |
reflex/vars/function.py:314-319 |
FunctionArgs — has rest: str | None field for ...rest spread support |
reflex/vars/function.py:348-459 |
ArgsFunctionOperation and ArgsFunctionOperationBuilder — used to create JS function definitions |
reflex/vars/function.py:36-186 |
FunctionVar base class — .call(), .partial(), type generics |
reflex/components/dynamic.py |
bundled_libraries list and import validation patterns |
reflex/compiler/compiler.py:340-387 |
_compile_memo_components() — compilation of existing custom components |