Skip to content

Compiler: Track provenance of rx.memo components and output to mirrored Python paths #6218

@masenf

Description

@masenf

Summary

When a rx.memo (or rx._x.memo) component is created, the compiler should inspect the call stack to determine which file in the user's app defined it. The compiled JS output for that memo component should then be written to a path that mirrors the Python source path, rather than being lumped into a single utils/components.jsx or utils/stateful_components.jsx file.

For example, a memo component defined in counter_app/ui/buttons.py would be rendered to .web/counter_app/ui/buttons.js.

Motivation

Today, all custom/memo components are compiled into a single monolithic file (.web/utils/components.jsx), and all shared stateful components go into another (.web/utils/stateful_components.jsx). This has two problems:

  1. Hot reload granularity: When a common imported helper changes (e.g., counter_app/ui/buttons.py is edited), the entire components.jsx file must be rewritten and vite must rebundle it, even if only one memo component changed. If each Python module's memo components were in their own JS file, only that specific file needs to be rewritten, and vite can do a more targeted incremental rebuild.
  2. Debuggability: When looking at .web/ output, there's no way to trace a compiled component back to the Python source that defined it. Mirrored paths make this obvious.

Design

Stack-frame provenance at creation time

When rx.memo (currently aliased to custom_component()) is called, the decorator should inspect the call stack to find the most recent frame in the user's app code:

def custom_component(component_fn):
    # Walk up the stack to find the caller's module
    frame = inspect.currentframe()
    caller_module = _find_user_app_frame(frame)
    # e.g., "counter_app.ui.buttons"
    
    @wraps(component_fn)
    def wrapper(*children, **props):
        comp = CustomComponent._create(...)
        comp._source_module = caller_module  # attach provenance
        return comp
    ...

"User app code" means any module that isn't part of the reflex package itself. The heuristic should walk up the stack skipping reflex.* frames until it finds the first external frame. However, the "external" frame could be another package… in which case, it's path should be relative to it's package root (typically subdir in site-packages).

Output path mirroring

During compilation, instead of writing all memo components to a single utils/components.jsx, group them by their _source_module and write each group to a mirrored path:

Python source                        JS output
counter_app/ui/buttons.py     →      .web/counter_app/ui/buttons.jsx
counter_app/ui/sidebar.py     →      .web/counter_app/ui/sidebar.jsx
counter_app/components.py     →      .web/counter_app/components.jsx

Components without provenance (e.g., those defined in reflex internals or in the REPL) fall back to the current utils/components.jsx location.

Import rewriting

Pages that use these memo components currently import from utils/components. The import paths need to be updated to point to the new mirrored locations:

// Before
import { MyButton } from "$/utils/components"

// After
import { MyButton } from "$/counter_app/ui/buttons"

Acceptance Criteria

  • rx.memo / custom_component() captures the calling module's path at decoration time
  • The provenance is stored on the CustomComponent and flows through to compilation
  • Heuristic correctly skips reflex.* internal frames and finds user app code
  • Compiled memo component JS is written to paths mirroring the Python source module
  • Import statements in page files correctly reference the new paths
  • Components without determinable provenance fall back to the current utils/components.jsx path
  • Hot reload of a single Python module only rewrites the corresponding JS file (not the monolithic components file)
  • All existing tests pass

Key Files

  • reflex/components/component.py:
    • custom_component() decorator (~line 2270) — where provenance capture should happen
    • CustomComponent class (~line 2008) — needs _source_module field
    • memo = custom_component alias (~line 2314)
  • reflex/compiler/compiler.py:
    • _compile_memo_components() — currently writes all to one file, needs grouping by source module
    • compile_memo_components() — public API, returns (path, code, imports)
    • get_components_path() — currently returns single path utils/components.jsx
  • reflex/compiler/utils.py:
    • get_components_path() (~line 504) — needs a per-module variant
  • reflex/compiler/templates.py:
    • memo_components_template() — may need adaptation for per-file output

Notes

  • This pairs well with the single-pass compiler work (ENG-9142-ENG-9149) but doesn't depend on it — it can be implemented against the current compiler.
  • The _source_module heuristic should handle edge cases: lambdas, nested functions, dynamically generated components, and components defined in __init__.py files.
  • Consider whether StatefulComponent wrappers (or their replacement from ENG-9145) should also track provenance. If a stateful wrapper is generated from a component in buttons.py, the wrapper's JS should probably live alongside it.
  • This is a stepping stone toward more granular hot reload — if each Python module maps to a specific JS file, the compiler can skip recompiling modules whose source hasn't changed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementAnything you want improved

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions