Skip to content

feat: add app_wraps to VarData for Var-driven provider injection#6447

Open
FarhanAliRaza wants to merge 12 commits into
reflex-dev:mainfrom
FarhanAliRaza:app-wrap-vardata
Open

feat: add app_wraps to VarData for Var-driven provider injection#6447
FarhanAliRaza wants to merge 12 commits into
reflex-dev:mainfrom
FarhanAliRaza:app-wrap-vardata

Conversation

@FarhanAliRaza
Copy link
Copy Markdown
Contributor

@FarhanAliRaza FarhanAliRaza commented May 4, 2026

Lets Vars declare app-level wrapper components in their VarData so the compiler can mount providers (state context, event loop, upload, etc.) based on what's actually used, instead of relying on hardcoded chains or special-case logic. State/event-loop providers now ride along on VarData.from_state and the events-hook helper, and UploadFilesProvider is mounted when selected_files/upload_file is referenced — even without an Upload component on the page. Layout renders the assembled chain so AppWrap reduces to hooks + children.

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?

fixes ENG-9152

Lets Vars declare app-level wrapper components in their VarData so the
compiler can mount providers (state context, event loop, upload, etc.)
based on what's actually used, instead of relying on hardcoded chains
or special-case logic. State/event-loop providers now ride along on
VarData.from_state and the events-hook helper, and UploadFilesProvider
is mounted when selected_files/upload_file is referenced — even without
an Upload component on the page. Layout renders the assembled chain so
AppWrap reduces to hooks + children.
@FarhanAliRaza FarhanAliRaza requested a review from a team as a code owner May 4, 2026 10:29
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 4, 2026

Merging this PR will not alter performance

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 22 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_compile_single_pass_all_artifacts[_complicated_page] 80.9 ms 77.3 ms +4.71%
test_compile_page_full_context[_stateful_page] 35.6 ms 37 ms -3.89%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing FarhanAliRaza:app-wrap-vardata (9ac52f1) with main (e88acf7)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (7281317) during the generation of this report, so e88acf7 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR replaces the hardcoded StateProvider/EventLoopProvider JSX in the JS Layout template with a Var-driven mechanism: VarData gains an app_wraps field, allowing any Var (state vars, event chains, upload helpers) to declare which React providers it needs. The compiler collects these declarations during the page walk and assembles the provider chain dynamically, so UploadFilesProvider is mounted when selected_files/upload_file is used even without an explicit Upload component.

All P2 findings — a missing type annotation on an internal helper and a tag-only equality semantics note — are non-blocking.

Confidence Score: 4/5

Safe to merge; all findings are P2 style/latent concerns with no current-code defects.

Only P2 issues found: an Any type annotation on an internal private function and a tag-based equality design note that is intentional for current stateless providers. The core logic — VarData promotion in __init__, dedup in merge, fixpoint collection in _resolve_app_wrap_components, and snapshot-boundary handling in the memoize plugin — is correct and well-tested.

packages/reflex-base/src/reflex_base/vars/base.py (_identity_key equality semantics) and reflex/compiler/plugins/builtin.py (_ingest_var_data_app_wraps type annotation).

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/vars/base.py Adds app_wraps field to VarData, overrides __eq__/__hash__ using a tag-based identity key; equality for app_wraps entries is keyed on (priority, tag) only, which could silently deduplicate differently-configured providers sharing the same tag.
packages/reflex-base/src/reflex_base/components/state_context.py New module defining StateContextProvider, EventLoopContextProvider, and get_events_hooks_var_data() factory; clean, well-documented, fresh-instance-per-call design avoids deepcopy cache-leakage issues.
reflex/compiler/plugins/builtin.py Adds collect_var_app_wraps_in_subtree, collect_var_app_wraps_for_component, and _ingest_* helpers; _ingest_var_data_app_wraps accepts var_data: Any instead of VarData, reducing type safety.
packages/reflex-components-core/src/reflex_components_core/core/upload.py Converts module-level upload_files_context_var_data constant to get_upload_files_context_var_data() factory that carries UploadFilesProvider as an app_wrap; removes the now-redundant Upload._get_app_wrap_components override.
packages/reflex-base/src/reflex_base/compiler/templates.py Moves StateProvider/EventLoopProvider hardcoded JSX from Layout to the dynamic render chain; AppWrap now just hosts hooks and returns children, making the JS template provider-agnostic.
reflex/compiler/compiler.py Adds fixpoint iteration in _resolve_app_wrap_components to surface Var-declared app_wraps from the app-wrap chain itself; well-guarded against infinite loops by tracking newly added keys.
reflex/compiler/plugins/memoize.py Correctly surfaces Var-declared app_wraps from snapshot-boundary subtrees before they are sealed by MemoizeStatefulPlugin, preventing providers from being missed behind memoization boundaries.
packages/reflex-components-core/src/reflex_components_core/base/app_wrap.py Refactors AppWrap from a Fragment subclass to a standalone Component with library=None and an explicit tag; correctly models it as the local JS function defined in app_root_template.

Sequence Diagram

sequenceDiagram
    participant V as Var/VarData
    participant C as Component
    participant CP as DefaultCollectorPlugin
    participant MP as MemoizeStatefulPlugin
    participant R as _resolve_app_wrap_components
    participant T as app_root_template

    V->>V: VarData(app_wraps=[(priority, Provider)])
    C->>CP: _get_vars() / _get_hooks_internal()
    CP->>CP: collect_var_app_wraps_for_component()
    CP->>CP: _ingest_var_data_app_wraps() → page_app_wrap_components
    MP->>CP: collect_var_app_wraps_in_subtree() (snapshot boundary)
    CP->>R: page_app_wrap_components
    R->>R: merge Component._get_app_wrap_components()
    R->>R: fixpoint: collect app_wraps from wrapper subtrees
    R->>T: ordered wrap chain (priority desc)
    T->>T: Layout renders StateProvider > EventLoopProvider > ... > AppWrap(hooks + children)
Loading

Reviews (1): Last reviewed commit: "feat: add app_wraps to VarData for Var-d..." | Re-trigger Greptile

Comment thread reflex/compiler/plugins/builtin.py
Comment thread packages/reflex-base/src/reflex_base/vars/base.py
Comment thread tests/units/test_app.py
EventLoopProvider now populates a module-level addEvents in
$/utils/context, so JSX literals constructed outside the React-tree
hoist path (e.g. ErrorBoundary.onError) can dispatch events without
useContext(EventLoopContext) being lexically in scope. State and
event-loop providers ride along on event-invocation VarData.app_wraps
(and via _get_event_app_wraps) so they still mount in the app root.
connectErrors stays on useContext since it drives re-renders;
AppWrap is now a Fragment rendering the chain in its body.
Comment thread reflex/compiler/plugins/memoize.py Outdated
Comment on lines +143 to +148
# ``addEvents`` no longer hoists a hook here (it's reached via the
# module-level import in ``Imports.EVENTS``), so a no-arg
# ``on_click=State.ping`` only shows up as ``event_triggers`` — without
# this check the boundary skips memoization and the callback leaks.
if component.event_triggers:
return True
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.

i think it's okay to allow the callback to leak into the main page here, because it's not something that will cause a re-render since addEvents is now a statically imported function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed

FarhanAliRaza and others added 9 commits May 29, 2026 21:12
# Conflicts:
#	packages/reflex-base/src/reflex_base/compiler/templates.py
#	reflex/compiler/compiler.py
#	reflex/compiler/plugins/memoize.py
#	tests/units/compiler/test_memoize_plugin.py
#	tests/units/test_app.py
#	tests/units/test_event.py
Per review: a no-arg handler (on_click=State.ping) surfaces only through
event_triggers and reaches addEvents via a module-level import, not a
hoisted hook. The inline callback carries no reactive data and never
drives a re-render, so wrapping it in a memo gains nothing. Drop the
event_triggers fast-path from the reactive-data check and let such
callbacks render in the page module. Handlers that reference state still
surface through the per-Var scan and memoize as before.
…dren

App._app_root deep-copies app-wrap components and rebinds their children
to assemble the provider chain. __copy__ already drops the render-path
caches for this reason, but copy.deepcopy preserved them, so a component
rendered during page compilation (e.g. a State/EventLoop/UploadFiles
provider injected via VarData.app_wraps) returned its stale, childless
_cached_render_result after children were appended. The page content
(children/Outlet) was silently dropped and the page rendered blank.

Add a __deepcopy__ that mirrors __copy__: the clone is for compile-time
mutation, so render caches are not carried over. Extract the shared cache
attr list to _COMPILE_CACHE_ATTRS. Regression test fails before the fix.
The page collector already pulls _get_hooks_internal/_get_added_hooks for
page-hook aggregation; pass those dicts into the Var app-wrap scan instead of
re-fetching, so each component avoids a second (uncached) _get_added_hooks and
_get_event_app_wraps per compile. Event-trigger providers now surface solely
through the Var scan, dropping the event_triggers special-case in the
subclass-override app-wrap path.
…port

The mixed-dispatch path (_dispatch_mixed_event_var, used by
on_click=lambda: rx.cond(..., function_var, event_spec)) still emitted the
legacy hooks={Hooks.EVENTS: None} hook — const [addEvents, connectErrors]
= useContext(EventLoopContext) — but Imports.EVENTS no longer imports
EventLoopContext. The rendered component threw "ReferenceError:
EventLoopContext is not defined", crashing the page and failing the
test_event_chain_click integration tests.

Match the other dispatch sites: reach addEvents through the module-level
import and ride the state/event-loop providers on app_wraps via
get_event_app_wraps(). Drop the now-unused Hooks import. Regression test
covers the mixed-dispatch VarData.
Construct StateProvider/EventLoopProvider once and reuse them instead of
rebuilding per state Var. This is now safe because consumers deep-copy before
rebinding children and the render-cache no longer survives deepcopy, so the
shared markers are never mutated; it lets the page-tree deepcopy and render
hash memoize them. VarData.add_state now routes through get_event_app_wraps()
for the single source of truth.
Copy link
Copy Markdown
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.

haven't gotten through all of the review yet. initial comments

Comment thread packages/reflex-base/src/reflex_base/components/component.py Outdated
Comment thread packages/reflex-base/src/reflex_base/vars/base.py Outdated
Extract the per-(priority, tag) app-wrap merge rule into a shared
insert_app_wraps primitive used by both VarData.merge and the compiler's
page-wide collection, so the dedup logic lives in one place. Two
different wrappers claiming the same slot now raise instead of silently
keeping the first. Hash app_wraps as a frozenset so merge order no longer
affects VarData identity (a + b == b + a).
_var_data=VarData(
imports=Imports.EVENTS,
hooks={Hooks.EVENTS: None},
app_wraps=get_event_app_wraps(),
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.

if we're putting the app wraps on the event chains themselves, do we still need Component._get_event_app_wraps? they should be picked up because they're on the event vars i would think

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

At walk time the EventChain is not converted to Var yet. So we can not read it. Maybe if we added some way to store on EventChain, then maybe we can delete it.

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.

Sounds good, let's get a backlog ticket capturing some of that context, but we don't have to prioritize it.

Copy link
Copy Markdown
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.

Working well for me. I'm thinking let's cut a 0.9.4 before we bring this in, but it's ready to merge.

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