payloadcms / payloadcms/payload
Custom Field components and admin.condition silently never render for fields nested inside an array (mergeServerFormState drops new nested paths)
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: trueon 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 tolocalized: falsereproduces the identical bug. - Not a config problem. Confirmed via raw admin-page HTML dumps that the field's
admin.components.Field/admin.conditionconfig 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 ownadmin.components.Fieldoverride.
To Reproduce
- Create a collection with an
arrayfield. - Give one of the array's nested subfields either:
admin.condition: (data, siblingData) => <some check against a sibling field in the same row>, oradmin.components.Field: '<path>#<ComponentName>'pointing to any custom client component.
- Create a document, add a row to the array, save.
- Reload the document edit view.
- 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
latestdist-tag as of writing) - Also confirmed not fixed in the unreleased canary/upcoming 4.0 line:
packages/ui/src/forms/Form/mergeServerFormState.tsis byte-identical between thev3.88.0tag and currentmain(checked against4.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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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