Skip to content

refactor(memo): promote memo out of rx._x.memo into rx.memo#6517

Open
FarhanAliRaza wants to merge 17 commits into
reflex-dev:mainfrom
FarhanAliRaza:non-exp-memo
Open

refactor(memo): promote memo out of rx._x.memo into rx.memo#6517
FarhanAliRaza wants to merge 17 commits into
reflex-dev:mainfrom
FarhanAliRaza:non-exp-memo

Conversation

@FarhanAliRaza
Copy link
Copy Markdown
Contributor

@FarhanAliRaza FarhanAliRaza commented May 15, 2026

All Submissions:

Summary

Promotes the memo system out of rx._x into a first-class rx.memo API,
with rx._x.memo kept as a thin deprecated alias so downstream apps
keep working.

  • Move the memo implementation from reflex/experimental/memo.py into
    packages/reflex-base/src/reflex_base/components/memo.py
    and re-export it as rx.memo; the old module becomes a deprecation
    shim that re-exports the same symbols.

  • Drop the Experimental prefix on the registry and public types so
    rx.memo reads cleanly without leaking the old namespace.

  • Support rx.EventHandler parameters in component memos and annotate
    return types so rx.memo-decorated callables type-check at call sites.

  • Expose EMPTY_VAR_STR / EMPTY_VAR_INT as memo-friendly sentinel
    defaults to replace ad-hoc empty-var construction in callers.

  • Migrate the existing Selenium memo integration test to Playwright
    under tests/integration/tests_playwright/test_memo.py;
    the legacy test_experimental_memo.py is retired and the remaining
    test_memo.py exercises the deprecation shim path.

  • Update docs and internal component packages to use rx.memo directly.

  • 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?

fixes ENG-9486

Move the memo/custom-component machinery from
``reflex/experimental/memo.py`` and ``reflex_base.components.component``
into a dedicated ``reflex_base.components.memo`` module and expose it as
``rx.memo``. ``rx.experimental.memo`` becomes a deprecated alias, and the
legacy ``CustomComponent`` index path in the compiler is dropped now that
all memos declare their library per-file.
Component-returning `@rx._x.memo` functions can now declare
`rx.EventHandler[...]` (and bare `rx.EventHandler`) parameters, which compile
to destructured JSX prop callbacks and are wired through `EventChain` at the
call site. Var-returning memos still reject event handlers.

Refactors per-parameter behavior into a `_MemoParamSpec` table keyed by a new
`MemoParamKind` enum (VALUE / CHILDREN / REST / EVENT_TRIGGER), so each kind
owns its classification, validation, placeholder construction, call-site
binding, and JSX signature emission. Adds a `_MemoCallBinding` accumulator
so `_post_init` no longer special-cases prop vs. rest vs. event routing.
Add explicit rx.Component return annotations to memoized helpers across
docs and internal packages, and narrow arrow_svg_component's class_name
to Var[str] now that rx.memo handles the conversion.
@FarhanAliRaza FarhanAliRaza requested review from a team and Alek99 as code owners May 15, 2026 18:38
@FarhanAliRaza FarhanAliRaza marked this pull request as draft May 15, 2026 18:39
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 15, 2026

Greptile Summary

This PR promotes rx.memo from rx._x.memo (experimental) to a first-class public API. The implementation moves the memo engine into reflex_base.components.memo, drops the Experimental prefix from internal types, adds EventHandler parameter support, exposes EMPTY_VAR_STR/EMPTY_VAR_INT sentinels, and migrates the integration test suite from Selenium to Playwright.

  • The old reflex.experimental.memo module is replaced by a one-liner shim that swaps itself in sys.modules for reflex_base.components.memo, ensuring from reflex.experimental.memo import memo continues to work while firing a deprecation warning at import time.
  • rx._x.memo is kept alive as a @property on ExperimentalNamespace that warns on access and returns the same underlying decorator, preserving backward compatibility.
  • The compiler pipeline is simplified: compile_memo_components now takes a single memos iterable (dropping the parallel CustomComponent path), and the legacy compile_custom_component helper is removed.

Confidence Score: 4/5

Safe to merge after fixing the misleading error messages in memo.py that still point users to the deprecated rx._x.memo namespace.

The memo engine works correctly and the deprecation shim is well-structured. However, all user-facing TypeError messages in packages/reflex-base/src/reflex_base/components/memo.py still say @rx._x.memo — so every validation error a @rx.memo user hits will tell them to use the old, deprecated decorator instead of the canonical one.

packages/reflex-base/src/reflex_base/components/memo.py — ~12 error-message strings reference @rx._x.memo instead of @rx.memo.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/components/memo.py New canonical memo implementation (1549 lines); all user-facing error messages still say @rx._x.memo instead of @rx.memo, and MemoParam/MemoParamKind are absent from all.
reflex/experimental/memo.py Reduced to a deprecation shim that replaces itself in sys.modules with reflex_base.components.memo; correctly triggers a deprecation warning at import time.
reflex/experimental/init.py Replaces the static memo=memo kwarg with a @Property on ExperimentalNamespace that fires a deprecation warning on access; clean implementation.
reflex/compiler/compiler.py Drops legacy CustomComponent/CUSTOM_COMPONENTS path; compile_memo_components now takes a single memos iterable using MEMOS and auto_memo_components.
reflex/compiler/utils.py Removes compile_custom_component and imports from experimental; updates compile_experimental_component_memo to use MemoComponentDefinition and MemoParamKind.
reflex/init.py Moves memo lazy-load mapping from component.py to memo.py and adds EMPTY_VAR_STR/EMPTY_VAR_INT to the public rx namespace.
packages/reflex-base/src/reflex_base/vars/base.py Adds EMPTY_VAR_STR and EMPTY_VAR_INT sentinel constants for memo default values.
tests/units/components/test_memo.py New comprehensive unit-test suite covering var memos, component memos with children/rest/EventHandler, compile paths, and deprecation shim.
tests/integration/tests_playwright/test_memo.py New Playwright integration test replacing the retired Selenium test_experimental_memo.py; exercises the full rx.memo compile-and-render path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["@rx.memo decorator"] --> B{Return type annotation}
    B -->|"rx.Component"| C["_create_component_definition()"]
    B -->|"rx.Var[T]"| D["_create_function_definition()"]
    B -->|"missing / other"| E["TypeError raised"]
    C --> F["_register_memo_definition()\nMEMOS registry"]
    D --> F
    F --> G["_create_component_wrapper()\n→ _MemoComponentWrapper"]
    F --> H["_create_function_wrapper()\n→ _MemoFunctionWrapper"]
    G --> I["User calls wrapper(...)"]
    H --> I
    I --> J["_bind_value / _bind_event_trigger\nper MemoParam"]
    J --> K["MemoComponent._create()"]
    F --> L["compile_memo_components()\nMEMOS.values()"]
    L -->|"MemoComponentDefinition"| M["compile_experimental_component_memo()"]
    L -->|"MemoFunctionDefinition"| N["compile_experimental_function_memo()"]
    M --> O[".jsx file per memo"]
    N --> O
    P["rx._x.memo property"] -->|"deprecation warning"| A
    Q["from reflex.experimental.memo import memo\nsys.modules swap"] -->|"deprecation warning at import"| A
Loading

Reviews (2): Last reviewed commit: "chore(memo): tidy compile_app splats and..." | Re-trigger Greptile

Comment thread packages/reflex-base/src/reflex_base/components/memo.py
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 15, 2026

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing FarhanAliRaza:non-exp-memo (8f6cd8a) with main (bf2deed)

Open in CodSpeed

Restore reflex/experimental/memo.py as a thin module redirect to
reflex_base.components.memo so existing rx.experimental.memo imports
keep working with a deprecation warning.
Rename ExperimentalMemo* classes, EXPERIMENTAL_MEMOS registry, and
related helpers/tests to plain Memo*/MEMOS now that memo is no longer
experimental. Move integration and unit tests out of experimental/ to
mirror the new module location.
…efaults

Reusable Var-typed empty-value constants so memo signatures can spell
strict defaults without per-call-site `Var.create(...)` (which trips B008)
or bespoke module-level singletons. Updates the memo doc and the
in-tree memos that previously rolled their own empty Var.
Drop redundant tuple() wraps around dict.values() splats in
compile_app, and hoist `import inspect` to module scope in the memo
unit tests. Refresh stale "experimental memo" wording in the memo
test docstrings now that the rename has landed.
@FarhanAliRaza FarhanAliRaza changed the title Experimental memo is !experimental refactor(memo): promote memo out of experimental into rx.memo May 18, 2026
@FarhanAliRaza FarhanAliRaza changed the title refactor(memo): promote memo out of experimental into rx.memo refactor(memo): promote memo out of rx._x.memo into rx.memo May 18, 2026
@FarhanAliRaza FarhanAliRaza changed the title refactor(memo): promote memo out of rx._x.memo into rx.memo refactor(memo): promote memo out of rx._x.memo into rx.memo May 18, 2026
@FarhanAliRaza FarhanAliRaza marked this pull request as ready for review May 18, 2026 18:27
Comment thread packages/reflex-base/src/reflex_base/components/memo.py Outdated
@masenf
Copy link
Copy Markdown
Collaborator

masenf commented May 20, 2026

hitting a problem with recursive components

from collections.abc import Sequence
from typing import TypedDict

import reflex as rx


class TreeNode(TypedDict):
    name: str
    children: Sequence["TreeNode"]


class State(rx.State):
    """The app state."""

    tree: TreeNode = TreeNode(
        name="root",
        children=[
            TreeNode(name="child1", children=[]),
            TreeNode(
                name="child2", children=[TreeNode(name="grandchild1", children=[])]
            ),
        ],
    )


@rx._x.memo
def node_x(data: rx.vars.ObjectVar[TreeNode]) -> rx.Component:
    return rx.vstack(
        rx.text(data.name),
        rx.foreach(data.children, lambda child: node_x(data=child)),
        class_name="pl-4 border-l",
    )


@rx.memo
def node(data: rx.vars.ObjectVar[TreeNode]) -> rx.Component:
    return rx.vstack(
        rx.text(data.name),
        rx.foreach(data.children, lambda child: node(data=child)),
        class_name="pl-4 border-l",
    )


def index() -> rx.Component:
    return rx.container(
        rx.color_mode.button(position="top-right"),
        rx.vstack(
            rx.heading("Welcome to Reflex!", size="9"),
            node(data=State.tree),
            spacing="5",
            justify="center",
            min_height="85vh",
        ),
    )


app = rx.App()
app.add_page(index)

if you comment out the @rx._x.memo on node_x, the app will run with the old memo component and work as expected.

Bind the memo wrapper to the function's name (in both module globals
and any matching free-variable cell) while the body is eagerly
evaluated, so a memo can recursively call itself — e.g. a tree node
component that renders its children through `rx.foreach`. Also refresh
remaining `@rx._x.memo` references in error messages and docstrings now
that the public name is `@rx.memo`.
# Conflicts:
#	packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py
#	pyi_hashes.json
#	tests/units/components/markdown/test_markdown.py
…oring

Missing return annotations default to `rx.Component`, and unannotated
parameters default to `rx.Var[Any]` (or `rx.Var[Component]` for `children`),
emitting a deprecation warning slated for removal in 1.0. Lets downstream
users keep compiling while they migrate to explicit annotations.
@FarhanAliRaza
Copy link
Copy Markdown
Contributor Author

image Warning for deprecation.

Infer the body's return type during memo eval and surface the public
`rx`/`rxe` qualname (e.g. `rxe.dnd.Draggable`) in the deprecation
message so users get a copy-pasteable annotation instead of the
generic hint. Update docs to use the inferred annotations.
Comment thread reflex/compiler/compiler.py Outdated

index_path = utils.get_components_path()
index_code = templates.memo_index_template(index_entries)
index_code = templates.memo_index_template([])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

seems this could just be removed?

Per-memo wrappers import from `$/utils/components/<name>` directly, so
the empty top-level index module had no callers. Remove the emission and
the now-unused `memo_index_template` and `get_components_path` helpers.
The bundled `$/utils/components` specifier emitted a root.jsx import
from the now-removed index module, breaking the production build with
`Could not load utils/components (Is a directory)`. The bare path no
longer resolves — each memo lives at `$/utils/components/<name>` — so
remove it from the default window bundle.
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.

2 participants