payloadcms / payloadcms/payload

Form state: a re-render of the admin page replaces everything the editor has typed (initialState effect), and the merge strips the value that would restore it

Open
#18,220 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area: ui Bug
Dominant language
TypeScript
Stars
44.8k
Forks
4.2k
Avg merge
2d 21h
Merged PRs (30d)
53

Description

Describe the Bug

Anything that makes the admin's RSC tree re-render while an editor is typing silently replaces
every value in the form with the document's pristine initialState. The typed text goes, and on
an upload collection the selected file goes with it, because file.value only ever exists in
client state. There is no error and nothing in the console.

The trigger in our case was a Set-Cookie written by Next middleware on the admin path. Payload
posts form state as a Server Action to the same URL, Next answers a Server Action whose response
carries a cookie with a re-rendered tree, and @payloadcms/next's Document view therefore hands
<Form> a brand new initialState object. Any other cause of a re-render should do the same.

The chain, measured in the browser with a logging wrapper around fieldReducer on 3.89.0:

UPDATE              alt: {lastRenderedPath, valid, passesCondition, value: "abcd", isModified: true}
MERGE_SERVER_STATE  alt: unchanged, value "abcd" still there      <- the merge is not the problem
RESPONSE 200 (next-action, our Set-Cookie on it)
REPLACE_STATE       alt: {lastRenderedPath, valid, passesCondition}   <- value and isModified gone

The replacement is this effect in packages/ui/src/forms/Form/index.tsx (v3.89.0, byte
identical in 3.90.1):

useEffect(() => {
  if (initialState) {
    contextRef.current = { ...initContextState } as FormContextType
    dispatchFields({ type: 'REPLACE_STATE', optimize: false, sanitize: true, state: initialState })
  }
}, [initialState, dispatchFields])

REPLACE_STATE with optimize: false returns action.state wholesale
(packages/ui/src/forms/Form/fieldReducer.ts), so local values are not merged, and resetting
contextRef to initContextState drops the file the editor picked, which is why the upload
preview disappears too (the thumbnail is gated on the form-state entry file.value, and
deepCopyObjectSimpleWithoutReactComponents means the server can never send it back).

Two things make it worse than a lost keystroke:

  1. The same response that triggers the re-render contains the correct value
    ("alt":{"lastRenderedPath":"alt","value":"abcd","initialValue":"abcd"}), but the form cannot
    recover it: on the typing path MERGE_SERVER_STATE is dispatched with no acceptValues
    (Form/index.tsx), so mergeServerFormState deliberately omits value and initialValue
    from the incoming state.
  2. Collections with autosave hide it, because autosave writes the value to the server before it
    is lost. It is only visible on collections without autosave, where a required field then makes
    the document impossible to save by typing.

A guard on that effect, so it applies initialState only when the form has not been modified
(or only once per document), would leave live values alone without changing what the effect is
for.

Link to the code that reproduces this issue

I do not have a public repro repository; this is a private commercial project. The steps below
reproduce it from a stock create-payload-app in under five minutes, and I am happy to build a
repro repo if that is what it takes to get this looked at.

Reproduction Steps
  1. A stock Payload 3.89.0 or 3.90.1 app on Next 16 with a Postgres adapter.
  2. An upload collection with a required text field, and no autosave:
    { slug: 'media', upload: { mimeTypes: ['image/*'] }, fields: [{ name: 'alt', type: 'text', required: true }] }
    
  3. Add middleware (proxy.ts on Next 16) that writes a cookie on every request, including
    /admin:
    export default function proxy() {
      const response = NextResponse.next()
      response.cookies.set('env', 'development', { path: '/' })
      return response
    }
    export const config = { matcher: '/((?!api|_next|.*\\..*).*)' }
    
  4. Sign in, open /admin/collections/media/create, type into alt.
  5. About a second later (the 250ms form-state debounce plus the round trip) the input empties
    itself. If a file was selected, its preview is gone too. The DOM node is the same one, so this
    is not a remount.

Removing the cookie write from that one response (writing it only on GET, for instance) makes the
symptom disappear, with no REPLACE_STATE after the Server Action response.

Which area(s) are affected?

area: ui

Environment Info
payload and every @payloadcms/* : 3.89.0, and reproduced identically on 3.90.1
next    : 16.3.5
react   : 19.3.0
database: @payloadcms/db-postgres, Postgres 18
browser : Chromium (Playwright 1.63)
node    : 24.20.0

Comparing the published @payloadcms/ui dist between 3.89.0 and 3.90.1 for forms/Form,
views/Edit and providers/DocumentInfo, the only difference is a client-upload guard in
Form/index.js, so nothing about this changed between those releases.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the reset with the stock app steps, then read the initialState effect in packages/ui/src/forms/Form/index.tsx and REPLACE_STATE in packages/ui/src/forms/Form/fieldReducer.ts. Trace the MERGE_SERVER_STATE path and contextRef reset, then verify that a server-action re-render preserves typed values, modified state, and selected upload files without autosave.

Written by the indexing model from the issue text.

Assessment

Tech stack
nextjs, react, typescript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.