Skip to content

feat: support embedding Reflex apps into host pages via mount_target#6462

Open
FarhanAliRaza wants to merge 6 commits intoreflex-dev:mainfrom
FarhanAliRaza:embedd-mode
Open

feat: support embedding Reflex apps into host pages via mount_target#6462
FarhanAliRaza wants to merge 6 commits intoreflex-dev:mainfrom
FarhanAliRaza:embedd-mode

Conversation

@FarhanAliRaza
Copy link
Copy Markdown
Contributor

@FarhanAliRaza FarhanAliRaza commented May 5, 2026

Add and config options for embedding a Reflex
app into an arbitrary DOM node on a host page. When is set, the entry client mounts a memory data router into the target element via
instead of hydrating the document, the build emits a stable
shim re-exporting the hashed Vite chunk, and a route
manifest is generated at compile time so the embedded app owns its own URL space. now sources from the data router's
location so the backend sees the in-widget URL rather than the host page's.

Usage

rxconfig.py

import reflex as rx

config = rx.Config(
    app_name="my_app",
    mount_target="#reflex-root",
    # only needed when host page and bundle live on different origins
    embed_origin="http://localhost:3000",
    cors_allowed_origins=["*"],  # tighten for prod
)

usage in the HTML file.

<div id="reflex-root">loading reflex...</div>
<script type="module" src="http://localhost:3000/app/entry.client.js"></script>

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)

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?

Add  and  config options for embedding a Reflex
app into an arbitrary DOM node on a host page. When  is set,
the entry client mounts a memory data router into the target element via
 instead of hydrating the document, the build emits a stable
 shim re-exporting the hashed Vite chunk, and a route
manifest is generated at compile time so the embedded app owns its own URL
space.  now sources  from the data router's
location so the backend sees the in-widget URL rather than the host page's.
@FarhanAliRaza FarhanAliRaza requested a review from a team as a code owner May 5, 2026 21:59
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 5, 2026

Merging this PR will not alter performance

✅ 24 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing FarhanAliRaza:embedd-mode (29cb5f4) with main (1ffc3b5)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR adds mount_target and embed_origin config options that allow a Reflex app to be embedded as a widget inside an arbitrary host page element instead of taking over the entire document. When mount_target is set, the entry client mounts a createMemoryRouter into the selected DOM node, a stable shim re-exports the hashed Vite entry chunk, a compile-time route manifest is generated, and locationRef in state.js mirrors the data-router's location so the backend sees the in-widget URL rather than the host page's.

  • Entry client branching: entry.client.js reads MOUNT_TARGET from env.json and switches between hydrateRoot(document, ...) (normal) and createRoot(target).render(...) with a createMemoryRouter (embed); the EmbedLayout export skips the document shell while keeping all Reflex providers.
  • Build pipeline changes: compile_embed_manifest generates a JS route manifest from registered routes, _emit_stable_entry_bootloader writes a shim at the stable app/entry.client.js path pointing to the hashed Vite asset, and _compile_vite_config prepends embed_origin to Vite's base for cross-origin asset resolution.
  • State routing fix: locationRef replaces direct window.location reads in applyEvent, so router data sent to the backend reflects the memory router's in-widget path.

Confidence Score: 4/5

The core embedding mechanism is well-reasoned and the happy-path test coverage is solid, but there are error-handling gaps and a caching quirk that could obscure problems in production.

All four findings are non-blocking quality concerns: Promise.all lacks error handling so embed mount failures are invisible to users, a misconfigured selector silently triggers document hydration causing cryptic React Router errors on real host pages, @functools.cache contradicts the per-compile-refresh docstring and would serve stale content after a mid-session package upgrade, and the module-level locationRef is null until first render so early events still read the host window.location in embed mode.

entry.client.js (error handling and selector fallback behavior) and frontend_skeleton.py (@functools.cache on the template reader) deserve a second look before merge.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/.templates/web/app/entry.client.js Adds embed-mode branching via MOUNT_TARGET; missing error handling on Promise.all and a misleading silent fallback when the selector resolves to null.
packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Introduces locationRef to mirror the data router location for embed mode; module-level singleton may serve the host URL on very early events before component mount.
reflex/utils/frontend_skeleton.py Adds update_entry_client with functools.cache that prevents the template from being re-read after the first compile despite the docstring claiming per-compile refresh.
reflex/compiler/compiler.py Adds compile_embed_manifest and route-translation helpers; correctly gates manifest generation on config.mount_target.
reflex/utils/build.py Adds _emit_stable_entry_bootloader to write a side-effect import shim for the hashed Vite entry; propagates mount_target to env.json correctly.
packages/reflex-base/src/reflex_base/constants/compiler.py Introduces Embed SimpleNamespace with ENTRY_PATH and MANIFEST_FILE constants; well documented.

Reviews (1): Last reviewed commit: "feat: support embedding Reflex apps into..." | Re-trigger Greptile

Comment thread packages/reflex-base/src/reflex_base/.templates/web/app/entry.client.js Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/app/entry.client.js Outdated
Comment thread reflex/utils/frontend_skeleton.py Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
Split the embed entry into a separate template and only swap it in when
mount_target is set, so non-embed builds produce the exact same
entry.client.js as before this feature landed.
…e mount

Log a clear error when MOUNT_TARGET points at a missing element or the
dynamic embed imports reject, instead of silently no-op'ing. Pre-seed
locationRef to / in embed mode so events dispatched before the first
effect commit don't briefly fall back to the host page's URL. Drop the
template read cache so toggling mount_target between compiles picks up
the right entry without a re-init.
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.

My main feedback here is to implement this primarily as a Plugin instead of wiring it through the whole framework. The plugin can take in the mount_target and embed_origin as parameters instead of adding more to the global config.

The only real downside of this approach is the current interpret_plugin_env parser for environment var specified plugins does not expose a way to provide arguments to a plugin class.

@FarhanAliRaza
Copy link
Copy Markdown
Contributor Author

My thought was plugin too, but most of the will still remain outside of plugin i think.

So I implemented this way.. I ll update it.

Embedding is now opt-in via `rx.plugins.EmbedPlugin(mount_target=...)`
in `rx.Config.plugins` rather than top-level `mount_target` /
`embed_origin` fields on `BaseConfig`. The plugin owns the embed entry
and route-manifest save tasks (no more conditional dispatch in
`compile_app`) and gains an optional `dev_preview` mode that injects a
Vite middleware serving a minimal host wrapper at `/`, so the embed can
be exercised without a separately served host page.

`mount_target` / `embed_origin` still fall back to `REFLEX_MOUNT_TARGET`
/ `REFLEX_EMBED_ORIGIN` so `REFLEX_PLUGINS=...EmbedPlugin` works without
constructor args. Compiler and build code now query the active plugin
via `get_embed_plugin()` instead of reading config attributes.
@FarhanAliRaza FarhanAliRaza requested a review from masenf May 6, 2026 09:35
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