fix(richtext-lexical): preserve field values when blocks are recycled or remounted#17342
Open
gonzoblasco wants to merge 1 commit into
Open
Conversation
… or remounted Fixes payloadcms#17232 — selecting an upload in a Lexical block with multiple upload fields silently erases the other field values. Root cause: when a drawer opens/closes, the BlockComponent is recycled (hide/show via Activity/Offscreen) or hard-remounted. The Form then re-initializes from the stale snapshot captured at block creation time, discarding values adopted since (e.g. uploads set by the user). The merge from payloadcms#14295 only iterated cachedFormState entries, so fields absent from the cache (built before they were ever set) were silently dropped. Three changes to BlockComponent: 1. Track the latest adopted form state in a ref (), updated in the onChange path (both before and after the server round-trip) and after the initial client-side fetch for new blocks. 2. Sync the ref into in the -effect cleanup, so that when the component is re-shown, the Form re-initializes from current values instead of the stale creation-time snapshot. Also restore from ref on mount to cover hard remounts. 3. Add fields from that are missing from the cached form state during the initial merge. Previously, only fields already present in were carried over — fields that had no value when the cache was built server-side but have since been set by the user were dropped. The fix is verified against the reproduction case in payloadcms#17232: blocks with 2+ upload fields no longer lose sibling values when selecting uploads.
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.
What
Fixes #17232 — In a Lexical
BlocksFeatureblock containing two or more upload fields, selecting a document in one upload field silently erases the other upload field values in that block. The loss is persisted: the next form-state round-trip writes the impoverished state back into the editor node.Why
When a drawer opens/closes, the
BlockComponentis recycled (hide/show via Activity/Offscreen) or hard-remounted. TheFormthen re-initializes from the staleinitialStatesnapshot captured at block creation time, discarding values adopted since (e.g. uploads set by the user).The merge from #14295 only iterated
cachedFormStateentries, so fields absent from the cache (because the cache was built before they were ever set) were silently dropped. This is the key gap: a field likeuploadAthat had no value when the server built the cache would not appear incachedFormState, and the merge would never add it back.How
Three changes to
BlockComponent(packages/richtext-lexical/src/features/blocks/client/component/index.tsx):1. Track the latest adopted form state in a ref
A new
latestFormStateRefis updated:onChange(before the server round-trip, capturing the current form state)newFormState(capturing the adopted state)getFormStatefetch for newly created blocksThis ensures we always have the most recent form state available, regardless of server round-trip timing.
2. Sync the ref into
initialStateon cleanup and mountsetInitialState(latestFormStateRef.current)so that when the component is re-shown, theFormre-initializes from current values instead of the stale creation-time snapshot.3. Add missing fields from
formDataduring the initial mergeThe previous merge from #14295 only iterated
cachedFormStateentries. Now we also iterateformDataand add any fields that are missing from the cache but present in the current block data (with a non-null value). This covers the case where a field had no value when the cache was built server-side but has since been set by the user.Verification
cacheBustermechanism for externalnode.setFields()calls is unchangedNotes
This addresses all three findings from the root-cause analysis in #17232:
initialStateon recycle) — fixed by ref tracking + cleanup syncformDatafieldName in formData) — the ref-based approach in fix feat: disable graphQL flag that will bypass gql on payload init #1 largely sidesteps this, since the ref captures the complete form state including nested fields. A dedicated fix for the flat-vs-nested path comparison in the merge would be a follow-up.Closes #17232