payloadcms / payloadcms/payload

Custom Field components and admin.condition silently never render for fields nested inside an array (mergeServerFormState drops new nested paths)

Open
#17,815 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

Describe the Bug

A field's admin.components.Field override and admin.condition are both computed correctly on the server, but the result never reaches the client when the field is nested inside an array field. There is no error anywhere — the custom component or conditional visibility simply silently never takes effect, and the field falls back to its default built-in widget / always-visible state instead.

This reproduces regardless of localization, drafts, or any other collection-level config — a single array field with a nested field carrying admin.components.Field or admin.condition is enough.

Root Cause (traced in framework source)

Confirmed by patching console.log statements directly into the running framework code (packages/ui/dist/forms/fieldSchemasToFormState/*.js and packages/ui/dist/utilities/buildFormState.js) and reading server-side log output, cross-referenced against the TypeScript source at tag v3.88.0.

Server-side computation is correct. renderField.ts's 'Field' in fieldConfig.admin.components branch fires and sets fieldState.customComponents.Field exactly as expected for a nested array-row subfield (e.g. path fields.0.myField). Confirmed via log output — this is not the bug.

The actual break is in packages/ui/src/forms/Form/mergeServerFormState.ts, which merges each server form-state response into the client's local state store:

for (const [path, incomingField] of Object.entries(incomingState || {})) {
  if (!(path in currentState) && !incomingField.addedByServer) {
    continue  // <- drops the entire incoming field-state entry
  }
  ...

A field's incoming state entry only survives this merge if its path is already a key in the client's existing currentState, or the entry is explicitly flagged addedByServer. This works fine for top-level fields, whose path is present from the very first, non-merged initial render. It breaks for any field nested inside an array field: that field's path (e.g. fields.0.myField) is only introduced into form state via a later merge call, when that row is rendered/expanded — at that exact moment the path is not yet a key in currentState, so the entire incoming entry (value, customComponents, passesCondition, everything) is silently dropped by this guard.

Confirmed consuming code: packages/ui/src/forms/RenderFields/RenderField.tsx, line ~54:

const CustomField = useFormFields(([fields]) => fields && fields?.[path]?.customComponents?.Field)
...
if (CustomField !== undefined) {
  return CustomField || null
}

Because the merge dropped the entry, fields[path].customComponents.Field is undefined here, so it silently falls through to the default field-type-to-component switch below, rendering the built-in widget for that field type instead of the custom one — with no error or warning anywhere in the chain.

Ruled Out

  • Not related to localization. Initially suspected localized: true on the parent array as the trigger (both first-discovered broken fields happened to live in localized arrays), but disproved with a direct test: setting the array to localized: false reproduces the identical bug.
  • Not a config problem. Confirmed via raw admin-page HTML dumps that the field's admin.components.Field/admin.condition config is served to the client correctly either way.
  • Not specific to array-type fields vs. leaf fields. Reproduces identically for both a plain leaf field (e.g. type: 'text') nested in the array, and for the array field itself carrying its own admin.components.Field override.

To Reproduce

  1. Create a collection with an array field.
  2. Give one of the array's nested subfields either:
    • admin.condition: (data, siblingData) => <some check against a sibling field in the same row>, or
    • admin.components.Field: '<path>#<ComponentName>' pointing to any custom client component.
  3. Create a document, add a row to the array, save.
  4. Reload the document edit view.
  5. Observe: the condition never gates visibility (field shows regardless of sibling value), and/or the custom Field component never renders (falls back to the default built-in widget for that field type). No console or server error.

Environment

  • Payload version: 3.88.0 (confirmed still current — this is npm's latest dist-tag as of writing)
  • Also confirmed not fixed in the unreleased canary/upcoming 4.0 line: packages/ui/src/forms/Form/mergeServerFormState.ts is byte-identical between the v3.88.0 tag and current main (checked against 4.0.0-canary.28)
  • Database adapter: MongoDB
  • Next.js 15, React 19

Notes

mergeServerFormState.ts's git history shows substantial prior work specifically on array/block row form-state edge cases (deleted rows reappearing after reorder, stale autosave responses, computed rows from server, etc. — e.g. #15906, #13551, #13501, #12962), so this file clearly gets real attention — but none of those prior fixes touch this specific guard, and the bug is still present as of the latest canary.

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

Start in packages/ui/src/forms/Form/mergeServerFormState.ts and trace how incoming nested paths are merged into currentState. Check the consuming lookup in packages/ui/src/forms/RenderFields/RenderField.tsx, then reproduce the array-row case from the issue. Done means nested admin.components.Field and admin.condition state reaches the client and the custom or conditional field behavior works after reload.

Written by the indexing model from the issue text.

Assessment

Tech stack
nextjs, react, typescript
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.