Repository navigation
fix(ui): prevent stale responses from overwriting form state - #17455
Draft
jacobsfletch wants to merge 70 commits into
Draft
jacobsfletch wants to merge 70 commits into
jacobsfletch wants to merge 70 commits into
Conversation
Autosave and form-state requests run on independent client queues and can resolve out of order. `mergeServerFormState` accepted a server value for any non-`isModified` field, so a straggler response whose payload was built from an older snapshot could clobber a value a later response already wrote. For auto-tracked fields like the native slug (never marked modified), the stale value was then re-sent and frozen by the field `beforeChange` hook, persisting the wrong value. Tags each value-accepting submit with a monotonic `requestSequence` in send order and records the writing sequence on each field as `valueSequence`. `mergeServerFormState` now rejects a response older than the sequence that last wrote the field. `valueSequence` is stripped before form state is serialized to the server, so it adds no wire cost.
…uence Replaces the per-field `isModified` flag with a monotonic sequence clock shared by local edits and value-accepting submits. `isModified` only guarded the single most-recently-edited field (it was cleared on every other field), so editing one field then another could let a background autosave response clobber the first. Each local edit and each accepted server response now stamps the field's `valueSequence`; `mergeServerFormState` accepts an autosave value only when the response's `requestSequence` is not older than the field's current sequence. This one check subsumes the old `isModified` guard and protects every recently-edited field independently, while still rejecting stale, out-of-order responses.
Contributor
📦 esbuild Bundle Analysis for payloadThis analysis was generated by esbuild-bundle-analyzer. 🤖
Largest pathsThese visualization shows top 20 largest paths in the bundle.Meta file: packages/next/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js
Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js
DetailsNext to the size is how much the size has increased or decreased compared with the base branch of this PR.
|
This was referenced Jul 24, 2026
Closed
5 tasks done
jacobsfletch
marked this pull request as draft
August 7, 2026 19:56
# Conflicts: # packages/ui/src/forms/Form/index.tsx
jacobsfletch
commented
Aug 17, 2026
| * | ||
| * @experimental This property is experimental and may change in the future. Use at your own risk. | ||
| */ | ||
| isModified?: boolean |
jacobsfletch
commented
Aug 17, 2026
Comment on lines
-431
to
-442
| // reset `isModified` in all other fields | ||
| // Use destructuring instead of delete to avoid mutating field objects | ||
| // shared with the previous state (shallow copy shares references) | ||
| if ('value' in action) { | ||
| for (const [path, field] of Object.entries(newState)) { | ||
| if (path !== action.path && 'isModified' in field) { | ||
| const { isModified: _, ...fieldWithoutIsModified } = field | ||
| newState[path] = fieldWithoutIsModified as typeof field | ||
| } | ||
| } | ||
| } | ||
|
|
…ditions # Conflicts: # test/form-state/int.spec.ts
…ditions # Conflicts: # packages/ui/src/forms/Form/index.tsx # test/form-state/collections/Autosave/index.tsx # test/form-state/payload-types.ts
- Restore replaceState as a full reset of queued and in-flight form requests - Paste array and block rows through dispatchFields so they don't reset the queue - Drop tests that edit or re-save while an explicit save is queued, since the form is disabled once a save is requested
This branch has not been deployed
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.
Requires #19203.
Concurrent autosave and form-state requests could resolve out of order and overwrite form values with stale data. This was especially visible with server-computed fields, such as fields modified by a
beforeChangehook. With a fast autosave interval on a throttled network, inputs could freeze to a partial value while typing quickly.Root cause:
Because autosave and form-state requests ran through independent client queues, their responses could merge out of order. A subsequent autosave could also be built from form state that had not yet incorporated the earlier form-state response, causing newer local or server-computed values to be lost.
Fix:
Builds on the shared form request queue in #19203. Each request captures the current form revision when it starts, and responses invalidated by newer edits cannot overwrite newer form values or validation state.
Server-computed values are accepted when the user has not edited since the request was sent. Responses invalidated by newer edits leave the current values and validation feedback intact. Completed explicit saves still retain their metadata and newly created document identity.
Huge win: this also removes the per-field
isModifiedflag introduced in #13460. Staleness is now handled consistently for every field and every form request instead of protecting only the most recently edited field.Fixes:
Previous work related this issue:
isModifiedflag that this PR replaces.Also related:
For the reviewer
Although the diff is large, there are really just 4 files that orchestrate pretty much this whole thing. The rest of the changes are mostly small, downstream compatibility updates based on these changes, plus a whole lotta tests.
Roughly two-thirds of the LOC changed in this PR are isolated to the
test/dir.Review should be focused on these files specifically:
packages/ui/src/forms/Form/index.tsxpackages/ui/src/forms/Form/fieldReducer.tspackages/ui/src/forms/Form/mergeServerFormState.tspackages/ui/src/views/Edit/index.tsxBefore
pr-17455-before-latest-chrome-take-1.mp4
After
pr-17455-after-latest-chrome-take-1.mp4