Skip to content

Add displayName to React components for DevTools visibility - #6945

Merged
masenf merged 8 commits into
mainfrom
claude/displayname-context-memo-vjfh3a
Aug 27, 2026
Merged

Add displayName to React components for DevTools visibility#6945
masenf merged 8 commits into
mainfrom
claude/displayname-context-memo-vjfh3a

Conversation

@masenf

@masenf masenf commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

Description

This PR adds displayName properties to all compiled React components and contexts, improving visibility in React DevTools. Without explicit display names, DevTools shows generic labels like Context.Provider, Anonymous, or Component for every instance in the tree, making it difficult to debug component hierarchies.

Changes:

  1. Context template (context_template): Added displayName assignments for:

    • Built-in contexts: ColorModeContext, UploadFilesContext, DispatchContext, EventLoopContext
    • State contexts: Named as StateContext(python.state.name) to identify which Python state they carry
    • ClientSide wrapper: Now a named function component with display name reflecting the wrapped component
  2. Page template (page_template): Every page compiles to a component named Component, so the route is now carried in its displayName (e.g., Component(about), Component(test/[dynamic])) to distinguish pages in DevTools

  3. Memo components (_render_memo_component): Added displayName assignment to exported memoized components, using either:

    • The Python function name for @rx.memo decorated components
    • The wrapped Python class name for auto-memoized components (instead of the hash-suffixed tag)
  4. MemoComponentDefinition: Added optional display_name field to carry the name React DevTools should show

  5. Dynamic imports: Updated ClientSide() calls to pass the component name as a second argument, enabling the wrapper to set an appropriate display name

  6. Theme context (react-theme.js): Added displayName to the theme context

Test coverage

  • Added 5 new unit tests in test_compiler.py covering context display names, page display names, and the ClientSide wrapper naming
  • Added 3 new unit tests in test_memo.py covering memo display names, custom wrappers, and string escaping
  • Added 1 new unit test in test_memoize_plugin.py covering auto-memo display names
  • All existing tests pass

Checklist

  • Tests pass with adequate coverage
  • uv run ruff check . and uv run ruff format . clean
  • uv run pyright reflex tests passes
  • pyi_hashes.json updated
  • News fragment added

https://claude.ai/code/session_01W4Psp6qZsEpbuRePuRVkYX

Review in cubic

claude added 2 commits August 25, 2026 11:10
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
@masenf
masenf requested a review from a team as a code owner August 25, 2026 19:16
@codspeed-hq

codspeed-hq Bot commented Aug 25, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 27 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/displayname-context-memo-vjfh3a (ed896ee) with main (894cb95)

Open in CodSpeed

Footnotes

  1. 8 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

greptile-apps Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds explicit React DevTools names to generated pages, contexts, client-only wrappers, and memoized components.

  • Propagates page routes and Python component names into safely serialized displayName assignments.
  • Names built-in, state, and theme contexts.
  • Preserves legacy ClientSide(component) calls while naming generated NoSSR wrappers.
  • Adds focused compiler and memoization tests plus release notes.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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

Comment thread packages/reflex-base/src/reflex_base/compiler/templates.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 12 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/compiler/templates.py
claude and others added 4 commits August 25, 2026 19:28
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 FarhanAliRaza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 under WithComponentProps. This is the exact failure mode the new docstring warns about, and it holds in dev and in a production reflex export build that I served and loaded.
  • Contexts: ColorModeContext, DispatchContext, EventLoopContext, ThemeContext, and one StateContext(...) 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.

Comment thread packages/reflex-base/src/reflex_base/compiler/templates.py Outdated
Comment thread reflex/compiler/utils.py
Comment thread packages/reflex-base/src/reflex_base/components/component.py Outdated
Comment thread packages/reflex-components-plotly/src/reflex_components_plotly/plotly.py Outdated
claude and others added 2 commits August 27, 2026 18:41
- `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
@masenf
masenf merged commit a4d99be into main Aug 27, 2026
110 checks passed
@masenf
masenf deleted the claude/displayname-context-memo-vjfh3a branch August 27, 2026 20:02
masenf pushed a commit that referenced this pull request Aug 28, 2026
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
masenf pushed a commit that referenced this pull request Aug 28, 2026
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
masenf pushed a commit that referenced this pull request Aug 28, 2026
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
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.

3 participants