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:
- 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.
- 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
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.
Summary
When a
rx.memo(orrx._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 singleutils/components.jsxorutils/stateful_components.jsxfile.For example, a memo component defined in
counter_app/ui/buttons.pywould 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:counter_app/ui/buttons.pyis edited), the entirecomponents.jsxfile 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..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 tocustom_component()) is called, the decorator should inspect the call stack to find the most recent frame in the user's app code:"User app code" means any module that isn't part of the
reflexpackage itself. The heuristic should walk up the stack skippingreflex.*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_moduleand write each group to a mirrored path:Components without provenance (e.g., those defined in reflex internals or in the REPL) fall back to the current
utils/components.jsxlocation.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:Acceptance Criteria
rx.memo/custom_component()captures the calling module's path at decoration timeCustomComponentand flows through to compilationreflex.*internal frames and finds user app codeutils/components.jsxpathKey Files
reflex/components/component.py:custom_component()decorator (~line 2270) — where provenance capture should happenCustomComponentclass (~line 2008) — needs_source_modulefieldmemo = custom_componentalias (~line 2314)reflex/compiler/compiler.py:_compile_memo_components()— currently writes all to one file, needs grouping by source modulecompile_memo_components()— public API, returns(path, code, imports)get_components_path()— currently returns single pathutils/components.jsxreflex/compiler/utils.py:get_components_path()(~line 504) — needs a per-module variantreflex/compiler/templates.py:memo_components_template()— may need adaptation for per-file outputNotes
_source_moduleheuristic should handle edge cases: lambdas, nested functions, dynamically generated components, and components defined in__init__.pyfiles.StatefulComponentwrappers (or their replacement from ENG-9145) should also track provenance. If a stateful wrapper is generated from a component inbuttons.py, the wrapper's JS should probably live alongside it.