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
35 changes: 27 additions & 8 deletions packages/reflex-base/src/reflex_base/compiler/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def app_root_template(
return f"""
{imports_str}
{dynamic_imports_str}
import {{ EventLoopProvider, StateProvider, defaultColorMode }} from "$/utils/context";
import {{ defaultColorMode }} from "$/utils/context";
import {{ ThemeProvider }} from '$/utils/react-theme';
import {{ Layout as AppLayout }} from './_document';
import {{ Outlet }} from 'react-router';
Expand All @@ -225,11 +225,7 @@ def app_root_template(

return jsx(AppLayout, {{}},
jsx(ThemeProvider, {{defaultTheme: defaultColorMode, attribute: "class"}},
jsx(StateProvider, {{}},
jsx(EventLoopProvider, {{}},
jsx(AppWrap, {{}}, children)
)
)
jsx(AppWrap, {{}}, children)
)
);
}}
Expand Down Expand Up @@ -364,6 +360,24 @@ def context_template(

export const isDevMode = {json.dumps(is_dev_mode)};

// Module-level event dispatchers populated by ``EventLoopProvider`` on each
// render. Components reach addEvents/connectErrors via this import instead of
// hoisting ``useContext(EventLoopContext)`` so JSX literals (e.g.
// ``ErrorBoundary.onError``) constructed in any JS scope can dispatch events
// without depending on lexical hook hoisting.
let _addEventsImpl = (events, args, event_actions) => {{
console.warn("addEvents called before EventLoopProvider mounted", events);
}};
let _connectErrorsImpl = [];

export function addEvents(events, args, event_actions) {{
return _addEventsImpl(events, args, event_actions);
}}

export function getConnectErrors() {{
return _connectErrorsImpl;
}}

export function UploadFilesProvider({{ children }}) {{
const [filesById, setFilesById] = useState({{}})
refs["__clear_selected_files"] = (id) => setFilesById(filesById => {{
Expand Down Expand Up @@ -394,14 +408,19 @@ def context_template(

export function EventLoopProvider({{ children }}) {{
const dispatch = useContext(DispatchContext)
const [addEvents, connectErrors] = useEventLoop(
const [addEventsLocal, connectErrors] = useEventLoop(
dispatch,
initialEvents,
clientStorage,
)
// Populate the module-level dispatchers so JSX literals constructed
// outside the React-tree path (e.g. ``ErrorBoundary.onError``) can call
// ``addEvents`` without needing the events hook hoisted in their scope.
_addEventsImpl = addEventsLocal;
_connectErrorsImpl = connectErrors;
return createElement(
EventLoopContext.Provider,
{{ value: [addEvents, connectErrors] }},
{{ value: [addEventsLocal, connectErrors] }},
children
);
}}
Expand Down
37 changes: 31 additions & 6 deletions packages/reflex-base/src/reflex_base/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -1879,14 +1879,15 @@ def _get_vars_hooks(self) -> dict[str, VarData | None]:
def _get_events_hooks(self) -> dict[str, VarData | None]:
"""Get the hooks required by events referenced in this component.

``addEvents`` is reached via the module-level import in
``Imports.EVENTS``, so no in-scope hook is needed for events.
State/event-loop providers ride along on the event invocation's
``VarData.app_wraps`` and via :meth:`_get_event_app_wraps`.

Returns:
The hooks for the events.
An empty dict.
"""
return (
{Hooks.EVENTS: VarData(position=Hooks.HookPosition.INTERNAL)}
if self.event_triggers
else {}
)
return {}

def _get_hooks_internal(self) -> dict[str, VarData | None]:
"""Get the React hooks for this component managed by the framework.
Expand Down Expand Up @@ -2041,6 +2042,30 @@ def _get_app_wrap_components() -> dict[tuple[int, str], Component]:
"""
return {}

def _get_event_app_wraps(self) -> dict[tuple[int, str], Component]:
"""Return state/event-loop providers required by event triggers.

Kept separate from :meth:`_get_app_wrap_components` so subclass
overrides of that method don't inadvertently strip these out —
the providers must be in the React tree for ``StateContexts``,
``EventLoopContext``, and the websocket plumbing to stay alive
even though ``addEvents`` itself is reached via module-level
import rather than a hook closure.

Returns:
The state/event-loop provider entries (empty if no event
triggers are bound).
"""
if not self.event_triggers:
return {}
# Lazy import: state_context imports from this module.
from reflex_base.components.state_context import get_event_app_wraps

return {
(priority, provider.tag or type(provider).__name__): provider
for priority, provider in get_event_app_wraps()
}

def _get_all_app_wrap_components(
self, *, ignore_ids: set[int] | None = None
) -> dict[tuple[int, str], Component]:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""App-wrap components mounting the state and event-loop React providers.

These wrap children in the ``StateProvider`` / ``EventLoopProvider`` JS
functions emitted into ``utils/context.js`` by ``compile_contexts``. They are
attached to the VarData returned by :meth:`reflex_base.vars.base.VarData.from_state`
so the compiler picks them up through the generic Var-driven app-wrap pipeline,
rather than the JS Layout template hard-coding them around every app.
"""

from __future__ import annotations

from reflex_base.components.component import Component
from reflex_base.constants import Dirs
from reflex_base.constants.compiler import Hooks
from reflex_base.vars.base import VarData


class StateContextProvider(Component):
"""App wrap that mounts the React state-context provider around children."""

library = f"$/{Dirs.CONTEXTS_PATH}"
tag = "StateProvider"


class EventLoopContextProvider(Component):
"""App wrap that mounts the websocket event-loop provider around children."""

library = f"$/{Dirs.CONTEXTS_PATH}"
tag = "EventLoopProvider"


def get_event_app_wraps() -> tuple[tuple[int, Component], ...]:
"""Return state/event-loop providers required when events are dispatched.

``StateProvider`` (100) wraps further out than ``EventLoopProvider``
(90) because the latter reads ``DispatchContext`` from the former.
Providers are constructed fresh per call — module-level caching
breaks because ``copy.deepcopy`` (used when assembling the app-root
chain) carries ``_cached_render_result`` across compile runs.

Returns:
``(priority, provider)`` entries deduped by the compiler.
"""
return (
(100, StateContextProvider.create()),
(90, EventLoopContextProvider.create()),
)


def get_events_hooks_var_data() -> VarData:
"""Build the VarData advertising the state/event-loop app wraps.

Returns:
A new VarData carrying both providers as app_wraps.
"""
return VarData(
position=Hooks.HookPosition.INTERNAL,
app_wraps=get_event_app_wraps(),
)
18 changes: 16 additions & 2 deletions packages/reflex-base/src/reflex_base/constants/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,34 @@ class CompileContext(str, Enum):
class Imports(SimpleNamespace):
"""Common sets of import vars."""

# ``addEvents`` is a module-level callable populated by
# ``EventLoopProvider``; importing it sidesteps the lexical-scope
# constraint a ``useContext(EventLoopContext)`` hoist would impose.
EVENTS = {
"react": [ImportVar(tag="useContext")],
f"$/{Dirs.CONTEXTS_PATH}": [ImportVar(tag="EventLoopContext")],
f"$/{Dirs.CONTEXTS_PATH}": [ImportVar(tag=CompileVars.ADD_EVENTS)],
f"$/{Dirs.STATE_PATH}": [
ImportVar(tag=CompileVars.TO_EVENT),
ImportVar(tag=CompileVars.APPLY_EVENT_ACTIONS),
],
}

# ``connectErrors`` is reactive — it drives connection-banner
# re-renders — so its consumers still go through ``useContext``.
CONNECT_ERRORS = {
"react": [ImportVar(tag="useContext")],
f"$/{Dirs.CONTEXTS_PATH}": [ImportVar(tag="EventLoopContext")],
}


class Hooks(SimpleNamespace):
"""Common sets of hook declarations."""

# Kept for legacy callers that still key off this string; the
# compiler no longer auto-hoists it.
EVENTS = f"const [{CompileVars.ADD_EVENTS}, {CompileVars.CONNECT_ERROR}] = useContext(EventLoopContext);"
CONNECT_ERRORS = (
f"const {CompileVars.CONNECT_ERROR} = useContext(EventLoopContext)[1];"
)

class HookPosition(enum.Enum):
"""The position of the hook in the component."""
Expand Down
14 changes: 10 additions & 4 deletions packages/reflex-base/src/reflex_base/event/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1055,14 +1055,14 @@ def _as_event_spec(
"""
from reflex_components_core.core.upload import (
DEFAULT_UPLOAD_ID,
upload_files_context_var_data,
get_upload_files_context_var_data,
)

upload_id = self.upload_id if self.upload_id is not None else DEFAULT_UPLOAD_ID
upload_files_var = Var(
_js_expr="filesById",
_var_type=dict[str, Any],
_var_data=VarData.merge(upload_files_context_var_data),
_var_data=VarData.merge(get_upload_files_context_var_data()),
).to(ObjectVar)[LiteralVar.create(upload_id)]
spec_args = [
(
Expand Down Expand Up @@ -2335,11 +2335,14 @@ def create(
arg_def_expr = Var(_js_expr="args")

if value.invocation is None:
# Lazy import: state_context → component → event (this module).
from reflex_base.components.state_context import get_event_app_wraps

invocation = FunctionStringVar.create(
CompileVars.ADD_EVENTS,
_var_data=VarData(
imports=Imports.EVENTS,
hooks={Hooks.EVENTS: None},
app_wraps=get_event_app_wraps(),
),
)
else:
Expand Down Expand Up @@ -2380,11 +2383,14 @@ def create(
_js_expr=f"{{{''.join(f'{statement};' for statement in statements)}}}",
)
if value.event_actions:
# Lazy import: state_context → component → event (this module).
from reflex_base.components.state_context import get_event_app_wraps

apply_event_actions = FunctionStringVar.create(
CompileVars.APPLY_EVENT_ACTIONS,
_var_data=VarData(
imports=Imports.EVENTS,
hooks={Hooks.EVENTS: None},
app_wraps=get_event_app_wraps(),
),
)
return_expr = apply_event_actions.call(
Expand Down
Loading
Loading