Skip to content

fix(richtext-lexical): preserve field values when blocks are recycled or remounted#17342

Open
gonzoblasco wants to merge 1 commit into
payloadcms:mainfrom
gonzoblasco:fix/17232-lexical-blocks-stale-form-state
Open

fix(richtext-lexical): preserve field values when blocks are recycled or remounted#17342
gonzoblasco wants to merge 1 commit into
payloadcms:mainfrom
gonzoblasco:fix/17232-lexical-blocks-stale-form-state

Conversation

@gonzoblasco

Copy link
Copy Markdown

What

Fixes #17232 — In a Lexical BlocksFeature block 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 BlockComponent is recycled (hide/show via Activity/Offscreen) or hard-remounted. The Form then re-initializes from the stale initialState snapshot captured at block creation time, discarding values adopted since (e.g. uploads set by the user).

The merge from #14295 only iterated cachedFormState entries, 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 like uploadA that had no value when the server built the cache would not appear in cachedFormState, 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 latestFormStateRef is updated:

  • At the start of onChange (before the server round-trip, capturing the current form state)
  • After the server responds with newFormState (capturing the adopted state)
  • After the initial client-side getFormState fetch for newly created blocks

This ensures we always have the most recent form state available, regardless of server round-trip timing.

2. Sync the ref into initialState on cleanup and mount

  • On cleanup (when the component is hidden/unmounted): setInitialState(latestFormStateRef.current) so that when the component is re-shown, the Form re-initializes from current values instead of the stale creation-time snapshot.
  • On mount (covers hard remounts where state is destroyed): restore from the ref if available.

3. Add missing fields from formData during the initial merge

The previous merge from #14295 only iterated cachedFormState entries. Now we also iterate formData and 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

Notes

This addresses all three findings from the root-cause analysis in #17232:

  • ✅ Finding 1 (stale initialState on recycle) — fixed by ref tracking + cleanup sync
  • ✅ Finding 2 (merge only covers existing cache keys) — fixed by also iterating formData
  • ⚠️ Finding 3 (nested group field paths don't match fieldName 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

… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lexical blocks: selecting an upload wipes other field values in the same block (Form re-initializes from stale initialState)

1 participant