fix(plugin-form): swapping recordId no longer leaves the previous record on screen - #3005
Merged
Conversation
…ord on screen
`loading` in ModalForm / DrawerForm / TabbedForm / SplitForm was only ever set
`true` once, by `useState(true)`, and thereafter only ever set `false`. A
`recordId` change therefore re-entered the fetch effect WITHOUT going back
through the loading branch: the form stayed mounted showing — and accepting
edits to — record A's values, with nothing indicating a different record had
been asked for, until B's response landed and replaced them in place. Anything
typed in that window read as A's on screen and would have been submitted
against B.
The same effect had no staleness guard either, so two overlapping reads landed
in COMPLETION order rather than request order: ask for B then C, and a slow B
arriving last left the form showing B while the caller had asked for C.
Both are the same defect from the user's side — the form displays a record
nobody asked for — so both are fixed:
- a change of record re-enters the loading state before the read, so the
previous record is off screen while the next one is in flight. Gated on the
record actually changing: the effect also re-runs on
`initialData`/`initialValues` identity churn (callers rebuild those objects
every render), and flashing the loading state for that would thrash;
- the effect's cleanup marks its read stale, so a response that is no longer
the one being awaited is dropped instead of overwriting a newer record.
ObjectForm already re-entered loading before its fetch, which is why this only
ever reproduced on the four sectioned variants.
Also fixed, a consequence of the above: hiding the form unmounts the inner
renderer, and that renderer is the only thing that reports dirtiness via
`onDirtyChange` — it gets no chance to report `false` on the way out. Without
clearing the flag, the overlay's unsaved-input guards (#2998) would stay armed
for input belonging to a record no longer on screen: a plain refresh would
prompt, and closing would offer to discard nothing. Verified by mutation —
dropping only that line fails the two guard tests with `expected true to be
false`.
New `recordSwapLoading.test.tsx` drives each container through a real record
swap with a `findOne` the test resolves by id, so both the stale-record and the
out-of-order cases are deterministic rather than timing-dependent; all 11 fail
against the previous code (8 of them on the two core assertions).
Verified: `npx vitest run packages/plugin-form/ packages/components/
packages/app-shell/` — 315 files / 2627 tests pass; `@object-ui/plugin-form`
type-check clean, lint 0 errors (the 2 unused-disable warnings are pre-existing,
in EmbeddableForm/MasterDetailForm).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
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.
Follow-up to #2991, which had to defend against this path. It turned out to be a defect of its own.
The bug
loadingin ModalForm / DrawerForm / TabbedForm / SplitForm is only ever settrueonce — byuseState(true)— and thereafter only ever setfalse. ArecordIdchange re-enters the fetch effect without going back through the loading branch, so:Anything typed in that window read as A's on screen but would have been submitted against B.
Reading the same effect closely surfaced a second defect: no staleness guard, so two overlapping reads land in completion order rather than request order. Ask for B then C, and a slow B arriving last leaves the form showing B while the caller asked for C.
Both are the same thing from the user's side — the form displays a record nobody asked for — so both are fixed here.
ObjectFormalready re-enters loading before its fetch (ObjectForm.tsx:460), which is why this only ever reproduced on the four sectioned variants.The fix
initialData/initialValuesidentity churn (callers rebuild those objects every render), and flashing the loading state for that would thrash.Also fixed — a consequence of the above
Hiding the form unmounts the inner renderer, and that renderer is the only thing that reports dirtiness via
onDirtyChange; it gets no chance to reportfalseon the way out. Without clearing the flag, the overlay's unsaved-input guards from #2998 would stay armed for input belonging to a record no longer on screen — a plain refresh would prompt, and closing would offer to discard nothing.I caught this only because #2998 landed hours earlier; it is exactly the kind of interaction a "just add setLoading(true)" change would have shipped silently.
Verification
findOnethe test resolves by record id, so both the stale-record and the out-of-order cases are deterministic rather than timing-dependent. Against the previous code all 11 fail — the two core assertions readexpected 'Record One' not to be 'Record One'andexpected 'Record Two' to be 'Record Three'.expected true to be false.npx vitest run packages/plugin-form/ packages/components/ packages/app-shell/→ 315 files / 2627 tests pass.@object-ui/plugin-formtype-check clean; lint 0 errors (the 2 unused-disable warnings are pre-existing, in EmbeddableForm/MasterDetailForm).🤖 Generated with Claude Code