payloadcms / payloadcms/payload
bug: multi-tenant tenants create form wiped on failed validation
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
When creating a document in the tenants collection (the collection registered as the multi-tenant plugin's tenants collection), submitting the form with a value that fails field validation:
- correctly shows the field-level validation error, then
- immediately resets the entire form back to its empty initial state, discarding every value the user had entered.
The user has to start the whole form over. This only happens on the tenants collection, because that is the only collection where the plugin mounts WatchTenantCollection. The same flow on any normal (tenant-scoped) collection behaves correctly: the validation error shows and the entered values are preserved.
Root cause
A failed-validation submit still flips the form's submitted flag to true. The plugin's WatchTenantCollection treats submitted as "document was saved" and triggers a tenant re-sync, which ends in a router.refresh(). The refresh feeds the document <Form> a new initialState reference, and <Form> reacts to that by replacing its entire state — wiping the in-progress create form.
Step-by-step chain
-
submittedflips totrueeven on a failed validation. In@payloadcms/uithe submit handler setssubmitted = trueon the client-side-validation-failure path (it does not gate it on a successful save):// @payloadcms/ui/dist/forms/Form/index.js (~L283-291) const isValid_2 = skipValidation || disableValidationOnSubmit ? true : await contextRef.current.validateForm(); setIsValid(isValid_2); if (!isValid_2) { errorToast(t('error:correctInvalidFields')); setProcessing(false); setSubmitted(true); // <-- true even though nothing was saved setDisabled(false); return; } -
WatchTenantCollectionreacts tosubmittedon create and callssyncTenants(). It assumessubmittedmeans "saved", but it is alsotrueafter a failed validation. There is no guard onid(which is stillundefinedon a failed create):// @payloadcms/plugin-multi-tenant/dist/components/WatchTenantCollection/index.js (L34-43) React.useEffect(() => { if (operation === 'create' && submitted) { void syncTenants(); } }, [operation, submitted, syncTenants, id]); -
syncTenants()mutates tenant-selection state. It refetches the options and callssetTenantOptions(...), which changes the identity of thesetTenantcallback (it depends ontenantOptions):// @payloadcms/plugin-multi-tenant/dist/providers/TenantSelectionProvider/index.client.js (L112-138) const syncTenants = React.useCallback(async () => { const req = await fetch(/* .../populate-tenant-options */); const result = await req.json(); if (result.tenantOptions && userID) { setTenantOptions(result.tenantOptions); // <-- state change // ... } }, [config.routes.api, tenantsCollectionSlug, userID]); -
The "no selected tenant" effect re-fires and calls
router.refresh(). On the tenants create view there is no selected-tenant cookie, soinitialValueis falsy and this effect is armed. WhensetTenant's identity changes (step 3), it re-runs and refreshes the route:// TenantSelectionProvider/index.client.js (L203-213) React.useEffect(() => { if (!initialValue) { setTenant({ id: undefined, refresh: true }); // -> router.refresh() } }, [initialValue, setTenant]); // setTenantAndCookie (L65-79) const setTenantAndCookie = React.useCallback(({ id, refresh }) => { setSelectedTenantID(id); /* set/delete cookie */ if (refresh) { router.refresh(); // <-- re-renders the server tree } }, [router]); -
router.refresh()hands<Form>a newinitialState, and<Form>resets. On re-render the document view recomputes form state and passes a newinitialStatereference.<Form>replaces its entire state from it — for a create form that is empty, so all entered values are lost:// @payloadcms/ui/dist/forms/Form/index.js (L637-649) useEffect(() => { if (initialState) { contextRef.current = { ...initContextState }; dispatchFields({ type: 'REPLACE_STATE', optimize: false, sanitize: true, state: initialState }); } }, [initialState, dispatchFields]);
The normal validation-error path (ADD_SERVER_ERRORS) preserves field values; the data loss here comes entirely from the out-of-band router.refresh(), not from the validation response.
Suggested fix
Gate the create-time sync in WatchTenantCollection on the document actually having been created — i.e. only sync once an id exists, since a failed create leaves id === undefined:
React.useEffect(() => {
if (operation === 'create' && submitted && id) { // <-- add `&& id`
void syncTenants();
}
}, [operation, submitted, syncTenants, id]);
This keeps the intended behavior (refresh the tenant selector after a tenant is actually created) while no longer firing on a failed-validation submit.
Link to the code that reproduces this issue
https://github.com/jhb-dev/payload-multi-tenant-create-form-reset
Reproduction Steps
- Clone the reproduction repository and run the development server (
pnpm dev). - Log in to
/adminwith the seeded userdemo@payloadcms.com/demo. This user has no tenant auto-selected, so the selected-tenant cookie /initialValueis empty. - Go to the tenants create view:
/admin/collections/tenants/create. - Fill in Name =
My First Tenantand Slug =Invalid Slug!(the slug fails the fieldvalidate, which only allows^[a-z0-9-]+$). - Click Save.
Expected: the validation error is shown and all entered values are preserved (standard Payload behavior).
Actual: the validation error flashes, then the whole form resets to empty and the user has to start over.
For contrast, repeat the same steps on the tenant-scoped Pages collection (/admin/collections/pages/create) with Title = My First Page and Slug = Invalid Slug!. There the validation error is shown and the entered values are correctly preserved — confirming the data loss is specific to the tenants collection where WatchTenantCollection is mounted.
Which area(s) are affected?
plugin: multi-tenant
Environment Info
Binaries:
Node: 22.19.0
npm: 10.9.3
pnpm: 11.5.1
Relevant Packages:
payload: 3.85.1
next: 16.2.6
@payloadcms/db-mongodb: 3.85.1
@payloadcms/graphql: 3.85.1
@payloadcms/next/utilities: 3.85.1
@payloadcms/plugin-multi-tenant: 3.85.1
@payloadcms/richtext-lexical: 3.85.1
@payloadcms/translations: 3.85.1
@payloadcms/ui/shared: 3.85.1
react: 19.2.6
react-dom: 19.2.6
Operating System:
Platform: darwin
Arch: arm64
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/plugin-multi-tenant/src/components/WatchTenantCollection/index.tsx and reproduce the issue with the linked repository using pnpm dev. Verify that failed validation on the tenants create form preserves entered values, while a successfully created tenant still refreshes tenant options; compare the normal Pages flow for expected behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100