Skip to content
Merged
1 change: 1 addition & 0 deletions news/6945.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The compiled frontend now names what React DevTools shows. Every memoized component carries a `displayName` taken from the Python component class or `@rx.memo` function it was generated from, instead of rendering as `Anonymous`; every generated context (`ColorModeContext`, `UploadFilesContext`, `DispatchContext`, `EventLoopContext`, `ThemeContext`, and one per state) is named, so the provider stack reads as `StateContext(reflex___state____state.my_state).Provider` rather than an unlabelled `Context.Provider`; each page is labelled with its route (`Component(blog/[slug])`) instead of a bare `Component`; and client-only (`NoSSRComponent`) wrappers render as `ClientSide(<Tag>)`.
1 change: 1 addition & 0 deletions packages/reflex-base/news/6945.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The compiled frontend now names what React DevTools shows. Every memoized component carries a `displayName` taken from the Python component class or `@rx.memo` function it was generated from, instead of rendering as `Anonymous`; every generated context (`ColorModeContext`, `UploadFilesContext`, `DispatchContext`, `EventLoopContext`, `ThemeContext`, and one per state) is named, so the provider stack reads as `StateContext(reflex___state____state.my_state).Provider` rather than an unlabelled `Context.Provider`; each page is labelled with its route (`Component(blog/[slug])`) instead of a bare `Component`; and client-only (`NoSSRComponent`) wrappers render as `ClientSide(<Tag>)`.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const ThemeContext = createContext({
resolvedTheme: defaultColorMode !== "system" ? defaultColorMode : "light",
setTheme: () => {},
});
ThemeContext.displayName = "ThemeContext";

export function ThemeProvider({ children, defaultTheme = "system" }) {
const [theme, setTheme] = useState(defaultTheme);
Expand Down
78 changes: 69 additions & 9 deletions packages/reflex-base/src/reflex_base/compiler/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,15 @@ def context_template(
for state_name in initial_state
])

# React DevTools labels a context provider from the context's
# ``displayName``; without it every state provider in the tree renders as
# ``Context.Provider``. Name each one after the Python state it carries.
state_context_display_names_str = "\n".join(
f"StateContexts.{format_state_name(state_name)}.displayName = "
f'"StateContext({state_name})";'
for state_name in initial_state
)

state_str = (
rf"""
export const state_name = "{state_name}"
Expand Down Expand Up @@ -409,6 +418,12 @@ def context_template(
export const EventLoopContext = createContext(null);
export const clientStorage = {"{}" if client_storage is None else json.dumps(client_storage)}

ColorModeContext.displayName = "ColorModeContext";
UploadFilesContext.displayName = "UploadFilesContext";
DispatchContext.displayName = "DispatchContext";
EventLoopContext.displayName = "EventLoopContext";
{state_context_display_names_str}

{state_str}

export const isDevMode = {json.dumps(is_dev_mode)};
Expand Down Expand Up @@ -445,8 +460,10 @@ def context_template(
);
}}

export function ClientSide(component) {{
return ({{ children, ...props }}) => {{
// ``displayName`` is what React DevTools shows for the wrapper; without it
// every client-only component in the tree renders as ``Anonymous``.
export function ClientSide(component, name) {{
function ClientSideComponent({{ children, ...props }}) {{
const [Component, setComponent] = useState(null);
useEffect(() => {{
async function load() {{
Expand All @@ -456,7 +473,9 @@ def context_template(
load();
}}, []);
return Component ? jsx(Component, props, children) : null;
}};
}}
ClientSideComponent.displayName = name ? `ClientSide(${{name}})` : "ClientSide";
return ClientSideComponent;
}}

export function EventLoopProvider({{ children }}) {{
Expand Down Expand Up @@ -512,15 +531,34 @@ def page_template(
custom_codes: Iterable[str],
hooks: dict[str, VarData | None],
render: dict[str, Any],
route: str = "",
):
"""Template for a single react page.

Every page compiles to a component named ``Component``, so the route is
carried in its ``displayName`` — otherwise React DevTools shows the same
``Component`` label for whichever page is mounted.

The function is declared, named, and only then exported. React Router's
``decorateComponentExportsWithProps`` rewrites an exported function
*declaration* into a function *expression* wrapped in
``UNSAFE_withComponentProps``, leaving no module-scope binding behind: a
trailing ``Component.displayName = ...`` would then throw
``ReferenceError: Component is not defined`` when the route module loads.
Exporting the identifier instead keeps the declaration in module scope, and
the wrapper renders ``Component`` as a child, so the name still shows.

Args:
imports: List of import statements.
dynamic_imports: List of dynamic import statements.
custom_codes: List of custom code snippets.
hooks: Dictionary of hooks.
render: Render function for the component.
route: The route this page is compiled for, used as its display name.
Defaults to empty, which omits the ``displayName`` assignment
entirely — ``page_template`` ships in ``reflex-base``, so an
out-of-tree caller predating the parameter keeps working and gets
the pre-existing unnamed ``Component``.

Returns:
Rendered React page component as string.
Expand All @@ -530,19 +568,27 @@ def page_template(
dynamic_imports_str = "\n".join(dynamic_imports)

hooks_str = _render_hooks(hooks)
display_name_str = (
f"Component.displayName = {json.dumps(f'Component({route})')};\n"
if route
else ""
)
return f"""{imports_str}

{dynamic_imports_str}

{custom_code_str}

export default function Component() {{
function Component() {{
{hooks_str}

return (
{_RenderUtils.render(render)}
)
}}"""
}}
{display_name_str}
export default Component;
"""


def package_json_template(
Expand Down Expand Up @@ -790,10 +836,16 @@ def dynamic_components_module_template(
def _render_memo_component(component: dict[str, Any]) -> str:
"""Render the ``export const`` statement for one memoized component.

The exported symbol carries a ``displayName`` so React DevTools labels the
memo with the name of the Python component it came from. Without it, the
wrapped arrow function is anonymous and every memo in the tree shows up as
``Anonymous``; ``memo()`` also drops the inferred name of the function it
wraps, so the assignment is needed even for readable symbols.

Args:
component: The component render dict (name, signature, render, hooks,
and the optional ``wrapper`` JS expression the function component
is wrapped in).
component: The component render dict (name, display_name, signature,
render, hooks, and the optional ``wrapper`` JS expression the
function component is wrapped in).

Returns:
Rendered component export as string.
Expand All @@ -808,7 +860,15 @@ def _render_memo_component(component: dict[str, Any]) -> str:
if wrapper and not _MEMO_WRAPPER_CALLEE_RE.fullmatch(wrapper):
wrapper = f"({wrapper})"
export_expr = f"{wrapper}{function_expr}" if wrapper else function_expr
return f"\nexport const {component['name']} = {export_expr};\n"
name = component["name"]
# ``display_name`` is resolved by the caller (``compile_experimental_component_memo``),
# which is the layer that knows the memo's clean export name — the JS symbol
# here carries a module hash and would make a poor label.
display_name = json.dumps(component["display_name"])
return (
f"\nexport const {name} = {export_expr};\n"
f"{name}.displayName = {display_name};\n"
Comment thread
masenf marked this conversation as resolved.
Comment thread
masenf marked this conversation as resolved.
)


def memo_components_template(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import dataclasses
import enum
import functools
import json
import logging
import operator
import typing
Expand Down Expand Up @@ -2339,11 +2340,12 @@ def _get_dynamic_imports(self) -> str:
if not self.is_default
else ".then((mod) => mod.default.default ?? mod.default)"
)
name = self.alias or self.tag
return (
f"const {self.alias or self.tag} = ClientSide(() => "
f"const {name} = ClientSide(() => "
+ library_import
+ mod_import
+ ")"
+ f", {json.dumps(name)})"
)


Expand Down
12 changes: 9 additions & 3 deletions packages/reflex-base/src/reflex_base/components/memo.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@ class MemoComponentDefinition(MemoDefinition):
# wrapper's ``VarData`` supplies its imports, so a custom wrapper brings
# its own and ``None`` pulls in nothing.
wrapper: Var | None = DEFAULT_MEMO_WRAPPER
# The name React DevTools shows for this memo. ``export_name`` (derived
# from the decorated function) is already readable for ``@rx.memo``, but
# auto-memoized wrappers carry a hash-suffixed tag, so the plugin sets this
# to the wrapped component's Python class name instead.
display_name: str | None = None

@property
def component(self) -> Component:
Expand Down Expand Up @@ -1849,13 +1854,14 @@ def passthrough(children: Var[Component]) -> Component:
passthrough.__module__ = __name__

definition = _create_component_definition(passthrough, Component, source_module)
replacements: dict[str, Any] = {}
# ``export_name`` is the content-hashed tag, which reads as noise in the
# React DevTools tree. Name the memo after the Python class it wraps.
replacements: dict[str, Any] = {"display_name": type(component).__qualname__}
if definition.export_name != tag:
replacements["export_name"] = tag
if captured_hole_child:
replacements["passthrough_hole_child"] = captured_hole_child[0]
if replacements:
definition = dataclasses.replace(definition, **replacements)
definition = dataclasses.replace(definition, **replacements)

return _create_component_wrapper(definition), definition

Expand Down
1 change: 1 addition & 0 deletions packages/reflex-components-plotly/news/6945.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The generated client-only wrapper for each plotly component now carries the component's name, so React DevTools shows `ClientSide(Plot)` instead of an anonymous wrapper.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import json
import logging
from typing import TYPE_CHECKING, Any, TypedDict, TypeVar

Expand Down Expand Up @@ -376,7 +377,7 @@ def dynamic_plotly_import(name: str, package: str) -> str:
return f"""
const {name} = ClientSide(() =>
{library_import}{mod_import}
)
, {json.dumps(name)})
"""


Expand Down
2 changes: 1 addition & 1 deletion pyi_hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,5 @@
"packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "f170ac685b6ba5892370166c80684db3",
"reflex/__init__.pyi": "a3e1782fab4a9aed55f66cc98af8c217",
"reflex/components/__init__.pyi": "9facd05a776d0641432696bbf8e34388",
"reflex/experimental/memo.pyi": "bc8b48357bef580e70a5881b65d3d3f7"
"reflex/experimental/memo.pyi": "35583b85befadf5cb125b14f7cd459cb"
}
7 changes: 5 additions & 2 deletions reflex/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,12 @@ def _compile_contexts(state: type[BaseState] | None, theme: Component | None) ->
)


def _compile_page(component: BaseComponent) -> str:
def _compile_page(component: BaseComponent, route: str) -> str:
"""Compile the component.

Args:
component: The component to compile.
route: The route the page is compiled for.

Returns:
The compiled component.
Expand All @@ -256,6 +257,7 @@ def _compile_page(component: BaseComponent) -> str:
custom_codes=component._get_all_custom_code(),
hooks=component._get_all_hooks(),
render=component.render(),
route=route,
)


Expand Down Expand Up @@ -741,7 +743,7 @@ def compile_page(path: str, component: BaseComponent) -> tuple[str, str]:
output_path = utils.get_page_path(path)

# Add the style to the component.
code = _compile_page(component)
code = _compile_page(component, path)
return output_path, code


Expand Down Expand Up @@ -769,6 +771,7 @@ def compile_page_from_context(page_ctx: PageContext) -> tuple[str, str]:
custom_codes=page_ctx.custom_code_dict(),
hooks=page_ctx.hooks,
render=page_ctx.root_component.render(),
route=page_ctx.route,
)
return output_path, code

Expand Down
1 change: 1 addition & 0 deletions reflex/compiler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ def compile_experimental_component_memo(
"name": memo_paths.library_and_symbol(
definition.source_module, definition.export_name
)[1],
"display_name": definition.display_name or definition.export_name,
Comment thread
masenf marked this conversation as resolved.
"signature": DestructuredArg(
fields=tuple(signature_fields),
rest=rest_param.placeholder_name if rest_param is not None else None,
Expand Down
Loading
Loading