Skip to content

New @rx._x.memo decorator (experimental) #6186

@masenf

Description

@masenf

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:

  1. Supports returning arbitrary Var types (not just Components) — generating a JS function and exposing the decorated name as a typed FunctionVar that imports the generated function definition.
  2. Also supports returning rx.Component — generating a React component (similar to existing @rx.memo but with the new argument conventions below)
  3. Enforces that all arguments are annotated as Var[...] types
  4. Handles special argument patterns: children and 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.memo component 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.memo decorator detects the return type annotation to decide behavior:
    • rx.Component return → generate a React.memo component
    • Var[T] return → generate a FunctionVar/ArgsFunctionOperation
  • All parameters must be annotated as Var[T] (or the special types below) — raise TypeError otherwise
  • *args and **kwargs in the Python signature are rejected with a clear error
  • Calling convention: the object returned by the decorator only accepts positional arguments that are rx.Component or rx.Var[rx.Component] (children). All other props must be passed as keyword arguments.
  • children handling: a parameter named children typed as rx.Var[rx.Component] is accepted and rendered as destructured children in the JS signature. If present, on the Python side, the decorated function accepts *children as positional arguments.
  • RestProp handling: define a new rx.RestProp type (based on ObjectVar). A parameter typed as RestProp renders as ...rest in the generated JS via FunctionArgs.rest. If present, on the python side, the function accepts **rest as arbitrary keyword arguments.
  • The function is called at decoration time with Var placeholders to produce the return expression
  • The decorated name behaves as a FunctionVar with the correct return type (for Var returns) or as a component factory (for Component returns)
  • Unit tests covering: Var-returning memo, Component-returning memo, children parameter, RestProp parameter, 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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions