Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { startTransition, createElement } from "react";
import { createRoot, hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
import env from "$/env.json";

const selector = env.MOUNT_TARGET;

if (selector) {
const target = document.querySelector(selector);
if (!target) {
console.error(
`[Reflex embed] No element matching MOUNT_TARGET selector ${JSON.stringify(selector)}; widget will not mount.`,
);
} else {
// @react-router/dev injects a preamble check at the top of every
// transformed JSX module that throws when this flag is unset. Framework-
// mode prerendered HTML installs it via <Scripts>; embed mode does not,
// so we install it before any user JSX module loads (the imports below
// are dynamic).
window.__vite_plugin_react_preamble_installed__ = true;
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;

// No __reactRouterContext on the host page; mount through a memory data
// router so the widget owns its URL space (host's window.location is
// unrelated) and react-router hooks like useLoaderData resolve. The route
// table is generated at compile time into __reflex_embed_manifest.js.
Promise.all([
import("$/styles/__reflex_global_styles.css"),
import("react-router"),
import("$/app/root"),
import("$/app/__reflex_embed_manifest"),
])
.then(([, reactRouter, root, manifest]) => {
const { createMemoryRouter, RouterProvider, Outlet } = reactRouter;
const children = manifest.default.map(({ path, load }) => {
const lazy = async () => ({ Component: (await load()).default });
if (path === "") return { index: true, lazy };
return { path, lazy };
});
const router = createMemoryRouter(
[
{
Component: () =>
createElement(root.EmbedLayout, null, createElement(Outlet)),
children,
},
],
{ initialEntries: ["/"] },
);
startTransition(() => {
createRoot(target).render(createElement(RouterProvider, { router }));
});
})
.catch((err) => {
console.error("[Reflex embed] Failed to load:", err);
});
}
} else {
startTransition(() => {
hydrateRoot(document, createElement(HydratedRouter));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export const refs = {};
// Array holding pending events to be processed.
const event_queue = [];

// Mirrors the data router's location so applyEvent can populate router_data
// with the in-widget URL. In embed mode the host page's window.location is
// unrelated to the Reflex route, so the backend's on_load and dynamic-route
// matching rely on this ref instead. Updated by useEventLoop once mounted;
// pre-seeded in embed mode with the memory router's initial path (see
// initialEntries in entry.client.embed.js) so events dispatched before the
// first effect commit don't briefly fall back to the host page's URL.
const locationRef = {
current: env.MOUNT_TARGET ? { pathname: "/", search: "", hash: "" } : null,
};

/**
* Generate a UUID (Used for session tokens).
* Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
Expand Down Expand Up @@ -378,16 +389,15 @@ export const applyEvent = async (event, socket, navigate, params) => {
event.router_data === undefined ||
Object.keys(event.router_data).length === 0
) {
// Since we don't have router directly, we need to get info from our hooks
const loc = locationRef.current ?? window.location;
const search = loc.search ?? "";
const hash = loc.hash ?? "";
event.router_data = {
pathname: window.location.pathname,
asPath:
window.location.pathname +
window.location.search +
window.location.hash,
pathname: loc.pathname,
asPath: loc.pathname + search + hash,
};
const query = {
...Object.fromEntries(new URLSearchParams(window.location.search)),
...Object.fromEntries(new URLSearchParams(search)),
...params.current,
};
if (query && Object.keys(query).length > 0) {
Expand Down Expand Up @@ -903,6 +913,10 @@ export const useEventLoop = (
}
}, [paramsR]);

useEffect(() => {
locationRef.current = location;
}, [location]);

const ensureSocketConnected = useCallback(async () => {
if (!mounted.current) {
// During hot reload, some components may still have a reference to
Expand Down
35 changes: 22 additions & 13 deletions packages/reflex-base/src/reflex_base/compiler/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,7 @@ def app_root_template(

{custom_code_str}

function AppWrap({{children}}) {{
{_render_hooks(hooks)}
return ({_RenderUtils.render(render)})
}}


export function Layout({{children}}) {{
function ReflexProviders({{children}}) {{
useEffect(() => {{
// Make contexts and state objects available globally for dynamic eval'd components
let windowImports = {{
Expand All @@ -223,17 +217,32 @@ def app_root_template(
window["__reflex"] = windowImports;
}}, []);

return jsx(AppLayout, {{}},
jsx(ThemeProvider, {{defaultTheme: defaultColorMode, attribute: "class"}},
jsx(StateProvider, {{}},
jsx(EventLoopProvider, {{}},
jsx(AppWrap, {{}}, children)
)
return jsx(ThemeProvider, {{defaultTheme: defaultColorMode, attribute: "class"}},
jsx(StateProvider, {{}},
jsx(EventLoopProvider, {{}},
jsx(AppWrap, {{}}, children)
)
)
);
}}


function AppWrap({{children}}) {{
{_render_hooks(hooks)}
return ({_RenderUtils.render(render)})
}}

export function Layout({{children}}) {{
return jsx(AppLayout, {{}}, jsx(ReflexProviders, {{}}, children));
}}

// Used by entry.client.js when mount_target is configured: skips the document
// shell (which renders react-router's <Meta>/<Scripts>/<Links> and requires a
// framework router context) but keeps the runtime providers.
export function EmbedLayout({{children}}) {{
return jsx(ReflexProviders, {{}}, children);
}}

export default function App() {{
return jsx(Outlet, {{}});
}}
Expand Down
2 changes: 2 additions & 0 deletions packages/reflex-base/src/reflex_base/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
CompileContext,
CompileVars,
ComponentName,
Embed,
Ext,
Hooks,
Imports,
Expand Down Expand Up @@ -90,6 +91,7 @@
"DefaultPage",
"DefaultPorts",
"Dirs",
"Embed",
"Endpoint",
"Env",
"EventTriggers",
Expand Down
21 changes: 21 additions & 0 deletions packages/reflex-base/src/reflex_base/constants/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,27 @@ class PageNames(SimpleNamespace):
STATEFUL_COMPONENTS = "stateful_components"


class Embed(SimpleNamespace):
"""Public artifacts for ``mount_target`` (embed) builds.

These paths form the host-page contract: a host script tag points at
``ENTRY_PATH`` (relative to the frontend origin), which loads the route
manifest at ``MANIFEST_FILE`` and dispatches into the embedded app. They
are intentionally stable across dev and prod so the same host HTML works
in both modes.
"""

# Host pages reference this path (e.g. ``<script src="/app/entry.client.js">``).
# In dev, Vite serves the source file at this URL; in prod, a shim emitted
# by ``_emit_stable_entry_bootloader`` re-exports the hashed Vite entry.
ENTRY_PATH = "app/entry.client.js"
# Embed-aware variant of the entry, swapped in only when ``mount_target``
# is set so non-embed builds remain byte-identical to the framework default.
ENTRY_EMBED_TEMPLATE = "app/entry.client.embed.js"
# Compile-time-emitted route manifest the embed entry imports at runtime.
MANIFEST_FILE = "__reflex_embed_manifest.js"


class ComponentName(Enum):
"""Component names."""

Expand Down
5 changes: 4 additions & 1 deletion packages/reflex-base/src/reflex_base/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Reflex Plugin System."""

from . import sitemap, tailwind_v3, tailwind_v4
from . import embed, sitemap, tailwind_v3, tailwind_v4
from ._screenshot import ScreenshotPlugin as _ScreenshotPlugin
from .base import CommonContext, Plugin, PreCompileContext
from .compiler import (
Expand All @@ -11,6 +11,7 @@
PageContext,
PageDefinition,
)
from .embed import EmbedPlugin
from .sitemap import SitemapPlugin
from .tailwind_v3 import TailwindV3Plugin
from .tailwind_v4 import TailwindV4Plugin
Expand All @@ -21,6 +22,7 @@
"CompileContext",
"CompilerHooks",
"ComponentAndChildren",
"EmbedPlugin",
"PageContext",
"PageDefinition",
"Plugin",
Expand All @@ -29,6 +31,7 @@
"TailwindV3Plugin",
"TailwindV4Plugin",
"_ScreenshotPlugin",
"embed",
"sitemap",
"tailwind_v3",
"tailwind_v4",
Expand Down
Loading
Loading