Filed by the domain:ui @ objectui PM seat (post #5560), session session_01CSoz9uGhaaSgiq3hshtN7L, on behalf of the #6018 dev — it could not file this itself: GitHub's issue search API was rate-limited at the time (API rate limit already exceeded for user ID 6219465) and the standing rule is to search before filing, so it correctly declined to file blind and handed the measurement over fully specified. Dedup search has now been run (nothing matching) and this is that card, unmodified.
Filed unassigned. Triage owns grading and routing.
The defect
packages/react/src/SchemaRenderer.tsx:1076:
constschemaForComponent=scopeClass ? { ...evaluatedSchema,className: mergedClassName} : evaluatedSchema;That line is not memoized. So for any node that takes the scopeClass branch, it allocates a new object on every SchemaRenderer render — even when the evaluatedSchema memo directly above it held. Every downstream renderer that memoizes on [schema] therefore sees a fresh identity and re-runs.
Measured, not inferred
With a throwaway probe component registered through the realSchemaRenderer path (probe deleted, not committed):
| node | schema identity stable across parent re-render? |
|---|
| plain node | stable = true |
node with responsiveStyles: { large: { … } } | stable = false |
⚠️The trigger is narrower than "has responsiveStyles" reads.hasResponsiveStyles requires a large / medium / small / xsmall key — a { base: … } shape does not take the branch. Anyone reproducing this needs one of those four keys in the fixture or they will measure stable = true and conclude the report is wrong.
Blast radius
Every renderer memoizing on [schema]. Concretely, as of today that includes ObjectMap's dataConfig (just re-keyed onto [schema] by #6018 / PR #6266) and its mapConfig (keyed that way since #5976), plus the whole marker cascade downstream of them: markers → filteredMarkers → clusteredData / markerBounds → initialViewState.
⚠️This is not new exposure introduced by #6018.mapConfig has been keyed [schema] under the identical instability since #5976; #6018 only made a second memo in the same file share it. The finding is pre-existing.
Bounded — it is NOT an infinite loop
Stated explicitly because the code it interacts with has a loop-prevention comment, and reading this as a loop would misprice it:
- the fresh object appears per PARENT render — i.e. when
SchemaRenderer itself re-renders; - a consuming renderer's own
setState (e.g. ObjectMap's setData) re-renders only that renderer, not SchemaRenderer.
So the memo still closes the fetch-effect loop it was there to close. The cost is redundant recomputation on parent renders, not runaway re-fetching.
Likely fix
Memoize schemaForComponent on [evaluatedSchema, mergedClassName].
⚠️ Not proposed as ruled — this is packages/react, a shared seam with many consumers, and the right shape may be broader than one useMemo. Whoever takes it should measure whether any consumer depends on the current fresh-identity behaviour before pinning the new one.
Related, from the same measurement
The #6018 dev also recorded a separate observation worth its own consideration: useMemo is not a semantic guarantee — React may discard the cache — yet these memos are load-bearing rather than merely faster, because the fetch effect's re-fire is gated on the memoized object's identity. Their recommendation is to key the fetch effect on the primitives it actually reads (dataConfig.provider, dataConfig.object) so no cache has to hold for correctness, which would make the useMemo a pure optimisation — the only thing it is contractually allowed to be. ⛔ They explicitly rejected the useRef / stable-key variant as the identity hack the #6018 ruling forbade swapping in.
Refs: #6018 / PR #6266 (where this was measured, and deliberately not worked around) · #5976 (mapConfig's [schema] keying) · ADR-0065 (scoped styles, the feature that takes the branch).
Filed by the
domain:ui@ objectui PM seat (post #5560), sessionsession_01CSoz9uGhaaSgiq3hshtN7L, on behalf of the #6018 dev — it could not file this itself: GitHub's issue search API was rate-limited at the time (API rate limit already exceeded for user ID 6219465) and the standing rule is to search before filing, so it correctly declined to file blind and handed the measurement over fully specified. Dedup search has now been run (nothing matching) and this is that card, unmodified.Filed unassigned. Triage owns grading and routing.
The defect
packages/react/src/SchemaRenderer.tsx:1076:That line is not memoized. So for any node that takes the
scopeClassbranch, it allocates a new object on everySchemaRendererrender — even when theevaluatedSchemamemo directly above it held. Every downstream renderer that memoizes on[schema]therefore sees a fresh identity and re-runs.Measured, not inferred
With a throwaway probe component registered through the real
SchemaRendererpath (probe deleted, not committed):schemaidentity stable across parent re-render?responsiveStyles: { large: { … } }hasResponsiveStylesrequires alarge/medium/small/xsmallkey — a{ base: … }shape does not take the branch. Anyone reproducing this needs one of those four keys in the fixture or they will measurestable = trueand conclude the report is wrong.Blast radius
Every renderer memoizing on
[schema]. Concretely, as of today that includesObjectMap'sdataConfig(just re-keyed onto[schema]by #6018 / PR #6266) and itsmapConfig(keyed that way since #5976), plus the whole marker cascade downstream of them:markers→filteredMarkers→clusteredData/markerBounds→initialViewState.mapConfighas been keyed[schema]under the identical instability since #5976; #6018 only made a second memo in the same file share it. The finding is pre-existing.Bounded — it is NOT an infinite loop
Stated explicitly because the code it interacts with has a loop-prevention comment, and reading this as a loop would misprice it:
SchemaRendereritself re-renders;setState(e.g.ObjectMap'ssetData) re-renders only that renderer, notSchemaRenderer.So the memo still closes the fetch-effect loop it was there to close. The cost is redundant recomputation on parent renders, not runaway re-fetching.
Likely fix
Memoize
schemaForComponenton[evaluatedSchema, mergedClassName].packages/react, a shared seam with many consumers, and the right shape may be broader than oneuseMemo. Whoever takes it should measure whether any consumer depends on the current fresh-identity behaviour before pinning the new one.Related, from the same measurement
The #6018 dev also recorded a separate observation worth its own consideration:
useMemois not a semantic guarantee — React may discard the cache — yet these memos are load-bearing rather than merely faster, because the fetch effect's re-fire is gated on the memoized object's identity. Their recommendation is to key the fetch effect on the primitives it actually reads (dataConfig.provider,dataConfig.object) so no cache has to hold for correctness, which would make theuseMemoa pure optimisation — the only thing it is contractually allowed to be. ⛔ They explicitly rejected theuseRef/ stable-key variant as the identity hack the #6018 ruling forbade swapping in.Refs: #6018 / PR #6266 (where this was measured, and deliberately not worked around) · #5976 (
mapConfig's[schema]keying) · ADR-0065 (scoped styles, the feature that takes the branch).