Skip to content

feat: add experimental memo decorator for JS-level component and function memoization#6192

Open
FarhanAliRaza wants to merge 13 commits intoreflex-dev:mainfrom
FarhanAliRaza:exp-memo
Open

feat: add experimental memo decorator for JS-level component and function memoization#6192
FarhanAliRaza wants to merge 13 commits intoreflex-dev:mainfrom
FarhanAliRaza:exp-memo

Conversation

@FarhanAliRaza
Copy link
Collaborator

@FarhanAliRaza FarhanAliRaza commented Mar 19, 2026

Introduce rx.experimental.memo (rx._x.memo) that allows memoizing components and plain functions at the JavaScript level. Supports component memos with typed props (including children and rest props via RestProp), and function memos that emit raw JS. Updates the compiler pipeline to handle both memo kinds alongside existing CustomComponent memos, and refactors signature rendering to use DestructuredArg.

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

closes #6186

…tion memoization

Introduce rx.experimental.memo (rx._x.memo) that allows memoizing
components and plain functions at the JavaScript level. Supports
component memos with typed props (including children and rest props
via RestProp), and function memos that emit raw JS. Updates the
compiler pipeline to handle both memo kinds alongside existing
CustomComponent memos, and refactors signature rendering to use
DestructuredArg.
@FarhanAliRaza FarhanAliRaza marked this pull request as draft March 19, 2026 11:52
@codspeed-hq
Copy link

codspeed-hq bot commented Mar 19, 2026

Merging this PR will not alter performance

✅ 8 untouched benchmarks


Comparing FarhanAliRaza:exp-memo (4e434f0) with main (64a0bc6)

Open in CodSpeed

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR introduces rx.experimental.memo (rx._x.memo), a new decorator that compiles Python functions to either memoized React components (React.memo) or exported pure-JS functions, with typed props, children support, and RestProp for rest-prop forwarding. It extends the compiler pipeline (_compile_memo_components, compile_experimental_component_memo, compile_experimental_function_memo), refactors signature rendering to use DestructuredArg, and adds RestProp as a new exported type in rx.Var.

Key changes:

  • reflex/experimental/memo.py: full decorator implementation including param analysis, component/function definition creation, registry (EXPERIMENTAL_MEMOS), collision detection, and Python-side runtime wrappers.
  • reflex/compiler/utils.py: two new compile functions for experimental memos; uses copy.deepcopy to avoid mutating stored definitions during style application.
  • reflex/compiler/compiler.py / templates.py: unified _compile_memo_components and new memo_components_template that renders component memos with React.memo(...) and function memos as plain export const assignments.
  • reflex/vars/object.py: adds RestProp(ObjectVar[dict[str, Any]]) marker class.

Issues found:

  • Rest-prop camelCase asymmetry in function memos (memo.py lines 686–695): extra kwargs passed to function-returning memos are NOT converted from Python snake_case to JavaScript camelCase before being placed in the bound-props dict, while component-returning memos do apply format.to_camel_case. This can silently produce incorrect JS property names (e.g. class_name instead of className).
  • get_props override returns dict instead of a list-like (memo.py lines 115–116): dict.fromkeys(props) is functional because iterating a dict yields keys, but any call site expecting a list type would be surprised.
  • Missing section comments in long multi-step functions in memo.py and compiler.py; the logical phases inside _bind_function_runtime_args, _create_component_wrapper, and _compile_memo_components lack brief inline comments.

Confidence Score: 3/5

  • The PR is functional and well-tested, but the rest-prop camelCase asymmetry between function and component memos is a silent behavioral inconsistency that can produce incorrect JS property names without any error.
  • The core logic is sound, collision detection and deep-copy fixes are in place, and coverage is good. The score is reduced from 5 because the camelCase asymmetry for rest props in function memos is a real behavioral difference that users are unlikely to discover without documentation, and the get_props lambda returning a dict (rather than an explicit list/tuple) is a latent type-safety gap.
  • Pay close attention to reflex/experimental/memo.py — specifically _bind_function_runtime_args (rest-prop camelCase) and ExperimentalMemoComponent._post_init (get_props return type).

Important Files Changed

Filename Overview
reflex/experimental/memo.py Core implementation of the experimental memo decorator. Contains the full definition pipeline (parameter analysis, component/function definition creation, runtime wrappers, and registry). Two issues found: rest-prop keys are not camelCased for function-returning memos (asymmetric with component memos), and get_props override returns a dict where a list/tuple is semantically expected.
reflex/compiler/compiler.py Updated _compile_memo_components to handle both ExperimentalMemoComponentDefinition and ExperimentalMemoFunctionDefinition. The React memo import is correctly gated on component renders only. Lacks section comments separating the three processing phases.
reflex/compiler/utils.py New compile_experimental_component_memo and compile_experimental_function_memo functions. The mutation concern from prior review is resolved by using copy.deepcopy before applying app style. Import filtering and signature generation are correct.
reflex/compiler/templates.py Updated memo_components_template to accept a functions list and renders them as plain export const statements (no React.memo wrapper). Component renders still use memo(...). Template logic is clean and consistent.
tests/units/experimental/test_memo.py Comprehensive unit tests covering var-returning and component-returning memos, RestProp forwarding, validation errors, name collision detection, and compilation output. The test for rest-prop camelCasing asserts snake_case keys (["class_name"]), confirming the asymmetry with component memos is intentional — but this should be more explicitly documented.
tests/integration/test_experimental_memo.py Integration test that spins up a real Reflex app with a function memo and a component memo (with children and rest props) and verifies the rendered UI via Selenium. Good end-to-end coverage of the happy path.
reflex/vars/object.py Adds RestProp(ObjectVar[dict[str, Any]]) as a thin subclass marker used to flag rest-prop parameters in memo signatures. Minimal and well-contained change.
reflex/app.py Imports EXPERIMENTAL_MEMOS and passes it to compile_memo_components alongside existing CUSTOM_COMPONENTS. Change is small and correct.

Sequence Diagram

sequenceDiagram
    participant User as User Code
    participant Memo as @rx._x.memo
    participant Registry as EXPERIMENTAL_MEMOS
    participant Compiler as compiler.py
    participant Utils as compiler/utils.py
    participant Template as templates.py

    User->>Memo: @rx._x.memo on fn (→ Component or → Var)
    Memo->>Memo: _analyze_params(fn)
    Memo->>Memo: _evaluate_memo_function(fn, params)
    alt Component-returning
        Memo->>Memo: _create_component_definition()
        Memo->>Memo: _lift_rest_props(component)
        Memo->>Registry: _register_memo_definition(ExperimentalMemoComponentDefinition)
        Memo-->>User: _create_component_wrapper() → callable
    else Var-returning
        Memo->>Memo: _create_function_definition()
        Memo->>Memo: _validate_var_return_expr()
        Memo->>Registry: _register_memo_definition(ExperimentalMemoFunctionDefinition)
        Memo-->>User: _create_function_wrapper() → callable
    end

    User->>User: App.compile()
    User->>Compiler: compile_memo_components(CUSTOM_COMPONENTS, EXPERIMENTAL_MEMOS)
    loop each ExperimentalMemoComponentDefinition
        Compiler->>Utils: compile_experimental_component_memo(definition)
        Utils->>Utils: copy.deepcopy(definition.component)
        Utils->>Utils: _apply_component_style_for_compile()
        Utils-->>Compiler: (render_dict, imports)
    end
    loop each ExperimentalMemoFunctionDefinition
        Compiler->>Utils: compile_experimental_function_memo(definition)
        Utils-->>Compiler: (function_dict, imports)
    end
    Compiler->>Template: memo_components_template(components, functions, ...)
    Template-->>Compiler: JS file with export const ... = memo(...) and export const ... = ((...) => ...)
Loading

Comments Outside Diff (1)

  1. reflex/compiler/compiler.py, line 368-411 (link)

    P2 Missing section comments in _compile_memo_components

    The function has three distinct processing phases (old custom components, experimental component memos, experimental function memos), followed by conditional import augmentation and final template rendering, but no inline comments delimit these phases. Adding brief section comments (e.g. # Compile legacy CustomComponents, # Compile experimental memo components, # Compile experimental memo functions, # Add shared React/emotion imports for component renders) would make the control flow much easier to follow at a glance.

    This applies to the diff range lines 362–411.

    Rule Used: Add blank lines between logical sections of code f... (source)

    Learnt From
    reflex-dev/flexgen#2170

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Last reviewed commit: "pyi: update hashes"

FarhanAliRaza and others added 6 commits March 19, 2026 18:51
… components

Add registry helpers that detect duplicate exported names across memo
kinds and raise on collision. Deepcopy the component before applying
styles during compilation so the stored definition stays clean. Simplify
the function wrappers .call to alias the wrapper itself.
@FarhanAliRaza FarhanAliRaza marked this pull request as ready for review March 20, 2026 08:42
Comment on lines +97 to +102
monkeypatch.setenv(
constants.PYTEST_CURRENT_TEST,
"tests/integration/test_experimental_memo.py::test_experimental_memo_app",
)
monkeypatch.setattr(reflex_app, "is_testing_env", lambda: True)
monkeypatch.setattr(reflex_state, "is_testing_env", lambda: True)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this about?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Copy link
Collaborator

@masenf masenf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some initial feedback.

please mark any greptile comments you have addressed as resolved, some of them seem out of date with the latest diff

"""
params = _analyze_params(fn, for_component=True)
component = _evaluate_memo_function(fn, params)
if not isinstance(component, Component):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a valid case where we would have a ComponentVar here, for example if the memo function returns an rx.cond construct

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated it .

Copy link
Collaborator

@masenf masenf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was testing this out and so far it's working really well and is awesome to be able to have the children and rest prop accessible inside the memo function.

one strange thing i noticed is that CSS props don't seem to be handled through the RestProp.

for example, if i pass color="rebeccapurple" to the wrapper function, it ends up getting passed straight through and shows up in the DOM instead of being processed at some earlier stage and converted into the css prop that emotion recognizes.

if i pass the prop as a style dict, style={...}, then it gets processed correctly.

i'm not sure it's possible to handle this in the general case, because we don't know which of the props are considered style props and which are functional props to the underlying component. this might just have to be a documentation thing, unless you can think of a way to handle it.

FarhanAliRaza and others added 6 commits March 25, 2026 13:53
…memo internals

Convert remaining_props keys to camelCase in _bind_function_runtime_args
so rest props (e.g. class_name → className) match the component memo
behavior. Also make MemoParam kw_only, return a tuple from get_props
instead of a dict, and remove unnecessary monkeypatch boilerplate from
the integration test fixture.
Replace _create_function_wrapper and _create_component_wrapper closures
with _ExperimentalMemoFunctionWrapper and _ExperimentalMemoComponentWrapper
classes, eliminating object.__setattr__ hacks for call/partial/_as_var
in favor of real methods.
Extract _normalize_component_return to wrap Var[Component] values in
Bare.create, allowing memos that return rx.cond or other component-typed
vars to be registered as component memos. Add a cond overload for
(Any, Var[Component], Var[Component]) -> Component.
…level

Replace instance-level self.tag assignment with cached dynamically
created ExperimentalMemoComponent subclasses via
_get_experimental_memo_component_class, so the tag is a class-level
attribute rather than set in _post_init.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New @rx._x.memo decorator (experimental)

2 participants