Skip to content

fix: proof-of-concept to support virtual fields added by hooks in buildFormState#15640

Open
brumm wants to merge 2 commits intopayloadcms:mainfrom
brumm:philipp/fix-afterread-hooks-in-build-form-state
Open

fix: proof-of-concept to support virtual fields added by hooks in buildFormState#15640
brumm wants to merge 2 commits intopayloadcms:mainfrom
brumm:philipp/fix-afterread-hooks-in-build-form-state

Conversation

@brumm
Copy link
Contributor

@brumm brumm commented Feb 16, 2026

How come?

I am building a setup where a base collection Persons is referenced by other collections like Employees and Users. To make editing that base data seamless, I include fields from Persons in a virtual group on other collections, in this example Employees like so:

Config
const embeddedPersonField: Field = {
  name: 'personalData',
  type: 'group',
  virtual: true,
  fields: Persons.fields,
}

export const Employees: CollectionConfig = {
  slug: 'employees',
  hooks: {
    afterRead: [populateEmbeddedPersonData],
    beforeChange: [saveEmbeddedPersonData],
  },
  fields: [
    {
      name: 'persons',
      type: 'relationship',
      relationTo: 'persons',
    },
    embeddedPersonField,
  ],
}

The data is populated and saved by hooks.

The bug: selecting a Person from the relationship dropdown re-renders the form after a request to form-state, but does not include field values from the selected Person.

See a gif

CleanShot 2026-02-16 at 11 25 20

What?

Run afterRead hooks in buildFormState and accept server-computed values in the onChange form state merge.

Why?

Virtual fields populated by afterRead hooks return empty when a relationship field changes in the admin panel. Two things prevent this:

  • buildFormState never runs afterRead hooks. On initial load, Document/index.tsx fetches via payload.findByID() (which runs hooks) and passes the result to buildFormState. On subsequent rebuilds triggered by field changes, hooks are skipped entirely.

  • mergeServerFormState discards the server-computed values. The onChange codepath dispatches MERGE_SERVER_STATE without acceptValues, so shouldAcceptValue is false and value/initialValue are stripped.

How?

  • Export afterRead from payload core
  • Call field-level and collection/global-level afterRead hooks in buildFormState (guarded by isTopLevelSchema)
  • Pass acceptValues: { overrideLocalChanges: false } in the onChange merge so server-computed values are accepted for unmodified fields

Note on architecture

  • We just run that one hook here as a proof-of-concept.
  • This imports afterRead directly from payload core into @payloadcms/ui. As far as I can tell, no other UI code currently does this. An alternative could be to expose something like payload.runAfterReadHooks(collection, doc) on the Payload instance.

… values on change

Virtual fields populated by afterRead hooks (e.g. embedded data from a
related document) return empty when a relationship field changes in the
admin panel.

Two things prevent this from working:

1. buildFormState never runs afterRead hooks. On initial document load,
   Document/index.tsx fetches via payload.findByID() which runs all hooks,
   then passes the result to buildFormState. But on subsequent form state
   rebuilds (triggered by field changes), buildFormState constructs data
   from client form state and skips hooks entirely.

2. Even after fixing (1), mergeServerFormState discards the server-computed
   values. The onChange codepath dispatches MERGE_SERVER_STATE without
   acceptValues, so shouldAcceptValue is false and value/initialValue are
   stripped from the merge — the client never sees the hook output.

This commit:
- Exports afterRead from payload core
- Calls field-level and collection/global-level afterRead hooks in
  buildFormState (guarded by isTopLevelSchema)
- Passes acceptValues: { overrideLocalChanges: false } in the onChange
  merge so server-computed values are accepted for fields the user
  hasn't manually modified
Tests that virtual fields populated by collection-level afterRead hooks
are correctly included in buildFormState output — both when a relation
is set (values populated) and when cleared (values nulled).
@brumm brumm changed the title fix: PoC to support virtual fields added by hooks in buildFormState fix: proof-of-concept to support virtual fields added by hooks in buildFormState Feb 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments