fix(plugin-map,plugin-calendar,plugin-gantt): re-key fetch effects onto dataConfig primitives - #6698
Merged
os-sales merged 4 commits intoAug 28, 2026
Conversation
…h effects onto dataConfig primitives
useMemo carries no semantic guarantee -- React may discard a memo cache and
recompute even when its deps compare equal, and getDataConfig(schema) builds
a fresh {provider, object}/{provider, items} wrapper object on every call.
So a fetch effect keyed on the whole `dataConfig` object was correct only
for as long as that identity happened to survive a discard: a recompute was
enough to re-run the effect and refetch, with schema itself unchanged.
Re-keys the load-bearing fetch effects onto the primitive fields they
actually read off dataConfig (provider / object / items) instead of the
container object, across every renderer the census found using the local
getDataConfig(schema)-into-useMemo pattern:
- plugin-map/ObjectMap.tsx -- both fetch effects (the known member, objectui#6270/#6591's deferred half)
- plugin-tree/ObjectTree.tsx -- both fetch effects
- plugin-calendar/ObjectCalendar.tsx -- the record-fetch effect (reusing the
existing schemaObjectName primitive; the schema-fetch effect was already
primitive-keyed)
- plugin-gantt/ObjectGantt.tsx -- reload()'s deps, and the fetch-object-schema
effect's dead (unused) dataConfig dependency dropped entirely. gantt's
effectiveDataSource memo deliberately keeps dataConfig as a dependency --
resolveDataSource needs the whole provider-shaped value, which cannot be
flattened to a fixed primitive list -- documented in-line as a scoped
exception; see the PR body's "known boundary" note.
packages/plugin-grid/src/ObjectGrid.tsx has the same dataConfig-in-deps
shape but sits inside objectui#6597's fence (plugin-grid/plugin-dashboard/
types) and is left untouched; packages/react is fenced in full (PR #6690).
Each touched renderer gets a new pinned test demonstrating the acceptance
direction: a dataConfig identity change carrying the SAME primitive fields
(the observable a discarded-and-recomputed memo produces) must not add an
extra dataSource.find/getObjectSchema call, alongside a counter-probe that a
genuinely different object name still does refetch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_8ca04858-ea8e-5b85-9182-de59aa49e00c
ObjectCalendar's own `dataConfig` memo is already primitive-keyed (objectui#6018), so varying `schema.objectName` between two new schema references -- the trick that works for plugin-map/plugin-tree's [schema]-keyed memo -- never even recomputes it here (all three of its own deps compare equal). Vary `schema.data` (an object, compared by reference) instead, which does force the recompute while leaving `provider`/`object` unchanged -- confirmed against the pre-fix source in this session's reverse verification (RED before, GREEN after). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_8ca04858-ea8e-5b85-9182-de59aa49e00c
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_8ca04858-ea8e-5b85-9182-de59aa49e00c
…#6696 PM coordinator flagged (after this branch was already cut, and after ObjectTree.tsx had already been edited on this branch) that PR #6696 (objectui#6481, "key ObjectTree's schema-settled gate to the bound object") is open, ready, and rewrites this exact file: it replaces the objectSchema/schemaSettled state pair this branch also touched with a shared `useSettledSchema` hook, re-keyed onto a derived `schemaKey` primitive as an incidental improvement -- already satisfying this card's acceptance direction for that one effect. ObjectTree's SECOND effect (the record fetch, ObjectTree.tsx:468 on current main) still closes over bare `dataConfig` and is untouched by #6696 -- a real, confirmed remaining census member -- but #6696 is "ready and heading for the queue" per the coordinator, so this file is contested until it lands. Reverts ObjectTree.tsx to its origin/main content (verified byte-identical to origin/main @ e0d83da, which does not yet contain #6696) and drops the new ObjectTree.discardedConfigMemo.test.tsx pin along with it, so this branch edits a file #6696 is mid-flight on nowhere. The changeset drops the @object-ui/plugin-tree entry to match. plugin-map / plugin-calendar / plugin-gantt are unaffected and ship as originally verified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_8ca04858-ea8e-5b85-9182-de59aa49e00c
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-sales
marked this pull request as ready for review
August 28, 2026 16:21
os-sales
deleted the
claude/issue-6592-rekey-fetch-effects-on-primitives
branch
August 28, 2026 16:44
This was referenced Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6592
Re-keys the load-bearing fetch effects onto the primitives they actually
read off
dataConfig(provider/object/items), souseMemogoesback to being a pure optimisation instead of a correctness dependency
(the sturdier half triage adopted from #6270, deferred out of PR #6591's
dispatch order).
The premise this PR is NOT resting on
PR #6591 made the
schemaidentity stable. It did not make itguaranteed.
useMemocarries no semantic guarantee — React ispermitted to discard a memo cache and recompute even when the dependency
array compares equal to the previous render.
getDataConfig(schema)builds a fresh
{ provider, object }/{ provider, items }wrapperobject on every call, so a discard alone — no author or caller action —
was enough to give the fetch effects below a new
dataConfigidentity andre-run them. This PR removes that dependence; it does not (and does not
need to) touch anything about
schema's own stability.Census — method and per-renderer result
Method. A systematic sweep (script, not grep-by-eye) over every
packages/**/*.{ts,tsx}file (excluding tests): collect everyconst x = useMemo(...)binding per file, then everyuseEffectwhosedependency array names one of those bindings as a bare identifier
(
dataConfig, notdataConfig.foo— the first pass conflated the two andproduced false positives, caught and fixed before trusting any result),
classified as a fetch effect when its body contains an
await,.then(,a
dataSource.*(call, orfetch(.Positive control.
packages/plugin-map(the known member) wasrequired to come back from the same query that reports every other
package. It did — confirmed both effects in
ObjectMap.tsx— before any"absent" result elsewhere was trusted.
git ls-tree -r HEAD -- packages/plugin-mapwas also checked non-empty before running anything, per the dispatch's
"don't believe a zero from a path nobody proved exists" instruction.
Per-renderer result (the
dataConfig-style family the sweep actuallytargets — every renderer with a local
getDataConfig(schema)helper fedinto a
useMemo):dataConfigpackages/plugin-map/ObjectMap.tsxdataConfigmemoised on[schema])packages/plugin-calendar/ObjectCalendar.tsxdataConfigmemo was already primitive-keyed by #6018, but the effect's deps still named the container)packages/plugin-gantt/ObjectGantt.tsxreload()'s deps (direct); the "fetch object schema" effect'sdataConfigdep was dead code (unused in its body)packages/plugin-tree/ObjectTree.tsxpackages/plugin-grid/ObjectGrid.tsxpackages/react/**Three more renderer effects outside this
dataConfigfamily carry thesame structural hazard (a memoised object read by identity in a fetch/
count-probe effect) — filed separately as #6697 rather than folded into
this diff, to keep this PR scoped to the family the card names:
plugin-detail/RelatedList.tsx(defaultSortSpec/listFilterNode),components/renderers/layout/containers.tsx(probeTargets, aMap),and
plugin-list/ListView.tsx(expandFields, untriaged).ObjectTree— found, then dropped from this diff mid-flightMy own census independently confirmed
ObjectTree.tsxas a member (botheffects). While implementing it, the PM coordinator relayed that PR #6696
(objectui#6481, open, ready, not yet merged) rewrites the SAME file: it
replaces
ObjectTree'sobjectSchema/schemaSettledstate pair with theshared
useSettledSchemahook and, as an incidental improvement, re-keysthe schema-resolution effect onto a derived
schemaKeyprimitive — alreadysatisfying this card's acceptance direction for that one effect. The
record-fetch effect (
ObjectTree.tsx:468on currentmain) still closesover bare
dataConfigand is untouched by #6696 — a real, confirmedremaining member — but editing the file now would be editing around a
shape that is about to change out from under it.
ObjectTree.tsxand itswould-be
ObjectTree.discardedConfigMemo.test.tsxpin were reverted out ofthis branch; the changeset no longer names
@object-ui/plugin-tree. Thisis a
needs_decision-shaped follow-up for the PM: re-dispatch therecord-fetch effect once #6696 lands (small, single-effect fix, same shape
as the three in this PR).
What each fixed effect was keyed on, before → after
ObjectMap.tsx— two effects, deps[dataProp, dataConfig, dataSource, hasInlineData, schema.filter, schema.sort, objectSchema]and[schema.objectName, dataSource, hasInlineData, dataConfig]→ both now listdataProvider/dataObjectName/dataItems(the three primitive fields either effect reads) in place ofdataConfig.ObjectCalendar.tsx— the record-fetch effect's deps[hasExternalData, dataConfig, dataSource, hasInlineData, schema.filter, schema.sort, refreshKey, objectSchemaReady, objectSchema]→dataConfigreplaced bydataProvider/dataItems, reusing theschemaObjectNameprimitive the file already computed for its (already-correct) schema-fetch effect.ObjectGantt.tsx—reload()'s deps[effectiveDataSource, resource, hasInlineData, dataConfig, schema.filter, schema.sort, objectSchema]→dataConfigreplaced bydataProvider/dataItems. The "fetch object schema" effect's deps[resource, effectiveDataSource, hasInlineData, dataConfig]→dataConfigdropped entirely (dead: never read in that effect's body).Known, documented boundary in
ObjectGantt.tsx:effectiveDataSource = useMemo(() => resolveDataSource(dataConfig, ...), [dataConfig, dataSource, apiFetch])deliberately keepsdataConfig(the whole object) as a dependency.resolveDataSourceneeds the whole provider-shaped value — theapiprovider reads itsread/writeHttpRequestconfig, which cannot be flattened to a fixed primitive list the wayobject/valuecan. For theobject/valueprovidersresolveDataSourcereturns thefallbackDataSource / a freshValueDataSourcewithout depending on further fields of the input, which is what keeps the discard-immunity demonstration below true for those two providers;apiwas not similarly decoupled here.Discarded-memo demonstration (the review question: "the effect no longer reads an object identity")
There is no public API to force React's internal memo-discard path, so
each new
*.discardedConfigMemo.test.tsxfile drives the same observablefailure a discard produces: a
dataConfigrecompute that yields a NEWobject reference carrying the SAME primitive fields.
getDataConfig(schema)builds a fresh wrapper on every call, so any recompute — whether triggered
by a discard or, as constructed here, by one of
dataConfig's ownuseMemodependencies changing reference without changing value — produces exactly
this "different identity, same content" shape. From the fetch effect's
perspective the two triggers are indistinguishable; what's under test is
whether the effect's OWN dependency array reacts to the identity or only to
the primitives.
The construction differs per file because each
dataConfigmemo has adifferent discard-proxy trigger:
ObjectMap/ObjectTree-shape (useMemo(() => getDataConfig(schema), [schema])): twoschemaobject literals, different references, byte-identical content — forces the recompute via the[schema]dep.ObjectCalendar(already primitive-keyed:[schema.data, schema.staticData, schema.objectName]): theschema-reference trick above does NOT recompute it (all three deps compare equal) — so the pin instead variesschema.dataitself, one object reference vs. another with identical content, which forces the recompute since that dep is compared by reference.ObjectGantt(useMemo(() => rawDataConfig, [JSON.stringify(rawDataConfig)])): neither trick above recomputes it (JSON-string dep already buys value stability against reference churn) — so the pin adds an inert_probefield toschema.data, read by nothing, whose value differs between renders and so forcesJSON.stringifyto differ.Each file's two tests:
dataConfigrecomputes to a new identity, same primitives →dataSource.find/getObjectSchemacall counts are unchanged after the rerender.dataConfigcarries a genuinely differentobject→ call counts increase, against the new object name. Without this, "immune to identity churn" would be equally satisfiable by an effect that never reacts to anything.Ablation — prediction stated before it ran, then observed
The important leg is the acceptance direction itself. Predicted: with the
effect re-keyed, the discarded-memo proxy does not re-run the fetch; before
the change, it does.
Mutation leg — commit the fix first, then restore the pre-fix content of
ObjectMap.tsx/ObjectTree.tsx/ObjectCalendar.tsx/ObjectGantt.tsxfrom the branch fork point (BASE=881d5c292) viagit checkoutpinned to that commit, path-scoped to each file. Ran the four new*.discardedConfigMemo.test.tsxfiles against the restored pre-fix source:Prediction miss, reported rather than forced:
ObjectCalendar's pin(with its ORIGINAL
schema-reference trigger, which is wrong for thatfile's already-primitive-keyed memo) passed even on pre-fix source — a
fact about the trigger's discriminating power, not the effect's
correctness. Rewritten to vary
schema.databy reference instead(documented above); re-run against the same restored pre-fix source:
All four now RED before the fix, for the predicted reason.
Restoration leg: restored all four files from
HEAD(this branch's fix commit — a path-scopedgit checkoutpinned toHEAD, never a bare unscoped restore, which would have restored from the polluted index instead). Verifiedgit diff HEADempty on all four, and agit hash-objectof the restored file vs.git rev-parseof theHEAD-committed blob matching on all four (ObjectMap.tsx338e6f78…,ObjectTree.tsxdf4609e3…,ObjectCalendar.tsx5dead55e…,ObjectGantt.tsxb8e552ad…— equal both sides).GREEN leg: re-ran the (by-then-final) four discard pins against the
restored fixed source:
Test Files 4 passed (4)/Tests 8 passed (8).ObjectTree.tsxwas subsequently reverted out of this branch entirely(see the census section above), so the shipped diff only carries the
map/calendar/gantt legs; the tree leg's RED/GREEN pair above is preserved
here as the record of what was measured before the surface turned out to
be contested.
Gate table
Verdicts quoted from each gate's own printed line; exit captured by
redirect-then-capture, never after a pipe.
tsc --noEmit -p packages/plugin-map/tsconfig.json(afterpnpm --filter @object-ui/plugin-map^... build)EXIT=0tsc --noEmit -p packages/plugin-calendar/tsconfig.json(afterpnpm --filter @object-ui/plugin-calendar^... build, which pulls in@object-ui/plugin-detail)EXIT=0tsc --noEmit -p packages/plugin-gantt/tsconfig.jsonEXIT=0pnpm exec vitest run packages/plugin-map/from repo root (objectui#3378)Test Files 16 passed (16)/Tests 92 passed (92)pnpm exec vitest run packages/plugin-calendar/from repo rootTest Files 15 passed (15)/Tests 104 passed (104)pnpm exec vitest run packages/plugin-gantt/from repo rootTest Files 50 passed (50)/Tests 420 passed (420)8da309855)pnpm exec vitest run packages/plugin-map/ packages/plugin-calendar/ packages/plugin-gantt/Test Files 84 passed (84)/Tests 622 passed (622)pnpm run check:control-bytes(new filesgit add-ed first)check-control-bytes: OK (scanned 5544 tracked text file(s); skipped 85 binary)node scripts/check-changeset-presence.mjs6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.node scripts/check-vi-mock-specifiers.mjsOK (3907 tracked source file(s) …)eslint --format jsonon the 6 touched.ts(x)files (3 source + 3 new test files)0 errors, 225 warnings — all pre-existing-class@typescript-eslint/no-explicit-any/react-hooks/*(warnseverity;lint.ymlsets no--max-warnings)origin/main) content vs. this branch's contentas anymock casts)Full
pnpm lint/ farm-widecheck:*is CI's to run; the narrowing abovecovers everything this diff touches, and the un-narrowed sibling
comparison proves the narrowing didn't hide anything.
Generated by Claude Code