Add displayName to React components for DevTools visibility - #6945
Conversation
React DevTools labels a component from its `displayName` (or the name of the function it wraps). The compiled frontend supplied neither: memo bodies are anonymous arrow functions passed to `memo()`, which erases the name JS would otherwise infer from the assignment, and none of the generated contexts were named. The result was a tree of `Anonymous` entries under an unlabelled stack of `Context.Provider`s. Each generated memo module now assigns a `displayName` to its export: `@rx.memo` components use the decorated function's name, and auto-memoized wrappers use the Python class they wrap rather than their content-hashed tag (`Foreach`, not `Foreach_comp_70fd2c58...`). The name is carried on `MemoComponentDefinition.display_name` and JSON-escaped at render time. Every context in the generated `context.js` is named too — `ColorModeContext`, `UploadFilesContext`, `DispatchContext`, `EventLoopContext`, `ThemeContext`, and one per state (`StateContext(<full state name>)`) — and the `ClientSide` HOC returns a named component tagged with the client-only component it loads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4Psp6qZsEpbuRePuRVkYX
Every page compiles to a component named `Component`, so the DevTools tree showed the same label whichever route was mounted. `page_template` now takes the route and emits `Component.displayName = "Component(<route>)"`, threaded through both the page-context and legacy compile paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4Psp6qZsEpbuRePuRVkYX
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryThe PR adds explicit React DevTools names to generated pages, contexts, client-only wrappers, and memoized components.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/reflex-base/src/reflex_base/compiler/templates.py | Generates display names for contexts, pages, client-side wrappers, and memo components while preserving module-scope page bindings. |
| packages/reflex-base/src/reflex_base/components/component.py | Passes safely serialized NoSSR component names into generated ClientSide wrappers. |
| packages/reflex-base/src/reflex_base/components/memo.py | Carries optional DevTools display names through memo definitions and labels auto-memoized components by Python class. |
| reflex/compiler/compiler.py | Propagates page routes into both page-compilation paths for route-specific DevTools labels. |
| reflex/compiler/utils.py | Resolves memo display names with the existing export name as a fallback. |
| packages/reflex-components-plotly/src/reflex_components_plotly/plotly.py | Supplies safely serialized Plotly component names to client-only wrappers. |
| packages/reflex-base/src/reflex_base/.templates/web/utils/react-theme.js | Assigns an explicit DevTools name to ThemeContext. |
| tests/units/compiler/test_compiler.py | Covers generated context, page, route, export-shape, and ClientSide naming output. |
| tests/units/compiler/test_memoize_plugin.py | Verifies auto-memoized components use their wrapped Python class names. |
| tests/units/components/test_memo.py | Verifies memo display-name propagation, custom-wrapper output, and string escaping. |
Reviews (6): Last reviewed commit: "Merge branch 'main' into claude/displayn..." | Re-trigger Greptile
There was a problem hiding this comment.
All reported issues were addressed across 12 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4Psp6qZsEpbuRePuRVkYX
React Router's `decorateComponentExportsWithProps` rewrites an exported
function *declaration* into a function *expression* wrapped in
`UNSAFE_withComponentProps`, so `export default function Component() {}`
leaves no module-scope `Component` binding. The trailing
`Component.displayName = ...` then threw `ReferenceError: Component is not
defined` as soon as a route module loaded, breaking every page.
Declare the function, name it, and export the identifier instead. React Router
then wraps the binding rather than the declaration, the assignment resolves,
and the wrapper renders `Component` as a child so the route name still shows in
the DevTools tree.
Verified against a real `react-router build` (8.3.0): the old shape's route
chunk throws `ReferenceError: Component is not defined` on evaluation, the new
one evaluates and keeps `Component.displayName`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4Psp6qZsEpbuRePuRVkYX
The changelog check runs towncrier per affected package, so reflex-base and reflex-components-plotly each need their own fragment alongside the root one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4Psp6qZsEpbuRePuRVkYX
FarhanAliRaza
left a comment
There was a problem hiding this comment.
Reviewed by running it, not just reading it
I checked this PR out and exercised it in a real app under examples/ — three routes (/, /about, /blog/[slug]), a root state plus a substate, an @rx.memo component, auto-memoized components, and rx.plotly for the ClientSide path. I then read the live React fiber tree, so the names below are what React DevTools actually resolves, not what the template emits.
Verified working
- Pages:
Component(index),Component(about),Component(blog/[slug]),Component(404), all present underWithComponentProps. This is the exact failure mode the new docstring warns about, and it holds in dev and in a productionreflex exportbuild that I served and loaded. - Contexts:
ColorModeContext,DispatchContext,EventLoopContext,ThemeContext, and oneStateContext(...)per state, substates and internal states included. Every context still unnamed in the tree comes from react-router or Radix, not from Reflex. - Memos:
memo(NamedWidget)for@rx.memo;memo(Div),memo(Button),memo(Plotly),memo(ErrorBoundary)for auto-memos. The hash-suffixed tags no longer surface. - Client-only:
ClientSide(Plot), and the lazily loaded plotly component still mounts and renders. - Regressions: event round-trip works, the dynamic route arg resolves, the production build succeeds, the prod route module loads with no
ReferenceError, and the display-name strings survive minification. No console errors or warnings on any page.ruff,pyright, and the 280 tests in the three touched test files pass.
Why changes are requested
Nothing here breaks the feature. The blocking item is (1): page_template is a shipped symbol in reflex-base, and the new parameter is required, so an out-of-tree compiler plugin that calls it raises TypeError. Items (2) and (3) are consistency cleanups worth folding into the same pass.
One informational note, no change needed: the index page reads Component(index) and the error page reads Component(404), because the value passed is the internal route key rather than the URL. That matches get_page_path, so it is self-consistent — just be aware the label is not always the URL.
- `page_template`'s `route` defaults to empty again. It is a shipped symbol in `reflex-base`, so a required parameter breaks any out-of-tree compiler plugin that calls it. With no route the `displayName` assignment is skipped entirely rather than emitting a contentless `Component()` label, which restores the exact pre-existing output for such callers. - The display-name fallback now lives in one place. `_render_memo_component` consumes the resolved value; `compile_experimental_component_memo` owns the fallback because it is the layer that knows the memo's clean export name (the JS symbol the template sees carries a module hash and made a poor label, so the two fallbacks were not equivalent). - `ClientSide` display names go through `json.dumps` in both the `NoSSRComponent` and plotly dynamic-import paths, matching the memo path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4Psp6qZsEpbuRePuRVkYX
Sample apps, Playwright drivers, NOTES.md and evidence screenshots from end-to-end browser testing of the routing fixes (#6593 on_load supersedes, #6790 splat matching, #6953 static/dynamic siblings, #6919 chained-event routing) and the rx.memo changes (#6949 call-site auto-memoization, #6605 RestProp style classification, #6945 displayName, #6730 wrapper=). Both clusters pass in dev and prod against the published PyPI alphas, with 0.9.8 baseline comparisons reproducing the old bugs. Anomalies recorded in each NOTES.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
components: all 6 component changelog items verified end-to-end; found the upload sanitizer returns '..' for all-dots filenames (upload-dir escape + 500), and rx.script(id=...) in head_components crashes compile. prod_export: prod/export/preview/dev all pass 18 browser checks; export ECONNREFUSED fix confirmed. Found: library='react-router-dom' silently installs unpinned RR7 in dev but breaks in prod; reflex run hangs after fatal node-version error on the npm path; npm<->bun switch corrupts reflex.lock; new vite 8.2.0 prod-build warnings. devtools_perf: #6945 naming fully verified via React fiber walks in dev+prod; #6905 owner-stack behavior exactly as documented; plotly 0.9.5a1 renders. Found: rx.plotly silently drops id= (expects divId). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
A ~29s 1920x1080 motion graphic covering the end-user-facing changes in 0.9.9, sourced from the 0.9.9a1 and 0.9.9a2 changelog entries across reflex, reflex-base, the component packages and reflex-hosting-cli. Nine beats: the dev-mode main-thread win (#6905), call-site auto-memoization of @rx.memo (#6949), the move to React Router 8 (#6854), DevTools component naming (#6945), stdlib logging with --json plus the client_error report (#6863, #6865, #6827), the cloud CLI's whoami/token and autoscaling flags (#6918, #6884, #6948), and a grid of the smaller fixes a user would notice (#6786, #6593, #6944, #6971, #6790, #6920). Styled to match reflex.dev by reusing its design system rather than approximating it: the dark violet/slate scales, wave-line motif, radial bloom and type scale come from reflex-site-shared, and the fonts are the same @fontsource-variable Instrument Sans and JetBrains Mono files the site serves. Source only — the rendered mp4 is a build artifact, regenerated by `npm run build` and ignored. Lives outside the packaged tree (hatch only includes /reflex), so it does not affect the published wheel or sdist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016arkVKmoogzUtZNfZZSvSo
Type of change
Description
This PR adds
displayNameproperties to all compiled React components and contexts, improving visibility in React DevTools. Without explicit display names, DevTools shows generic labels likeContext.Provider,Anonymous, orComponentfor every instance in the tree, making it difficult to debug component hierarchies.Changes:
Context template (
context_template): AddeddisplayNameassignments for:ColorModeContext,UploadFilesContext,DispatchContext,EventLoopContextStateContext(python.state.name)to identify which Python state they carryClientSidewrapper: Now a named function component with display name reflecting the wrapped componentPage template (
page_template): Every page compiles to a component namedComponent, so the route is now carried in itsdisplayName(e.g.,Component(about),Component(test/[dynamic])) to distinguish pages in DevToolsMemo components (
_render_memo_component): AddeddisplayNameassignment to exported memoized components, using either:@rx.memodecorated componentsMemoComponentDefinition: Added optional
display_namefield to carry the name React DevTools should showDynamic imports: Updated
ClientSide()calls to pass the component name as a second argument, enabling the wrapper to set an appropriate display nameTheme context (
react-theme.js): AddeddisplayNameto the theme contextTest coverage
test_compiler.pycovering context display names, page display names, and theClientSidewrapper namingtest_memo.pycovering memo display names, custom wrappers, and string escapingtest_memoize_plugin.pycovering auto-memo display namesChecklist
uv run ruff check .anduv run ruff format .cleanuv run pyright reflex testspassespyi_hashes.jsonupdatedhttps://claude.ai/code/session_01W4Psp6qZsEpbuRePuRVkYX