Files
payloadcms/packages/next/src/views/CreateFirstUser/index.client.tsx
Jacob Fletcher 82f1bb9864 perf: skips field validations until the form is submitted (#10580)
Field validations can be expensive, especially custom validations that
are async or highly complex. This can lead to slow form state response
times when generating form state for many such fields. Ideally, we only
run validations on fields whose values have changed. This is not
possible, however, because field validation functions might reference
_other_ field values with their args, and there is no good way of
detecting exactly which fields should run in this case. The next best
thing here is to only run validations _after the form has been
submitted_, and then every `onChange` event thereafter until a
successful submit has taken place. This is an elegant solution because
we currently don't _render_ field errors until submission anyway.

This change will significantly speed up form state response times, at
least until the form has been submitted. From then on, all field
validations will run regardless, just as they do now. If custom
validations continue to slow down form state response times, there is a
new `event` arg introduced in #10738 that can be used to control whether
heavy operations occur on change or on submit.

Related: #10638
2025-01-27 20:21:33 +00:00

125 lines
3.1 KiB
TypeScript

'use client'
import type { FormProps, UserWithToken } from '@payloadcms/ui'
import type {
DocumentPreferences,
FormState,
LoginWithUsernameOptions,
SanitizedDocumentPermissions,
} from 'payload'
import {
ConfirmPasswordField,
EmailAndUsernameFields,
Form,
FormSubmit,
PasswordField,
RenderFields,
useAuth,
useConfig,
useServerFunctions,
useTranslation,
} from '@payloadcms/ui'
import { abortAndIgnore, handleAbortRef } from '@payloadcms/ui/shared'
import React, { useEffect } from 'react'
export const CreateFirstUserClient: React.FC<{
docPermissions: SanitizedDocumentPermissions
docPreferences: DocumentPreferences
initialState: FormState
loginWithUsername?: false | LoginWithUsernameOptions
userSlug: string
}> = ({ docPermissions, docPreferences, initialState, loginWithUsername, userSlug }) => {
const {
config: {
routes: { admin, api: apiRoute },
serverURL,
},
getEntityConfig,
} = useConfig()
const { getFormState } = useServerFunctions()
const { t } = useTranslation()
const { setUser } = useAuth()
const abortOnChangeRef = React.useRef<AbortController>(null)
const collectionConfig = getEntityConfig({ collectionSlug: userSlug })
const onChange: FormProps['onChange'][0] = React.useCallback(
async ({ formState: prevFormState, submitted }) => {
const controller = handleAbortRef(abortOnChangeRef)
const response = await getFormState({
collectionSlug: userSlug,
docPermissions,
docPreferences,
formState: prevFormState,
operation: 'create',
schemaPath: userSlug,
signal: controller.signal,
skipValidation: !submitted,
})
abortOnChangeRef.current = null
if (response && response.state) {
return response.state
}
},
[userSlug, getFormState, docPermissions, docPreferences],
)
const handleFirstRegister = (data: UserWithToken) => {
setUser(data)
}
useEffect(() => {
const abortOnChange = abortOnChangeRef.current
return () => {
abortAndIgnore(abortOnChange)
}
}, [])
return (
<Form
action={`${serverURL}${apiRoute}/${userSlug}/first-register`}
initialState={initialState}
method="POST"
onChange={[onChange]}
onSuccess={handleFirstRegister}
redirect={admin}
validationOperation="create"
>
<EmailAndUsernameFields
className="emailAndUsername"
loginWithUsername={loginWithUsername}
operation="create"
readOnly={false}
t={t}
/>
<PasswordField
autoComplete="off"
field={{
name: 'password',
label: t('authentication:newPassword'),
required: true,
}}
path="password"
/>
<ConfirmPasswordField />
<RenderFields
fields={collectionConfig.fields}
forceRender
parentIndexPath=""
parentPath=""
parentSchemaPath={userSlug}
permissions={true}
readOnly={false}
/>
<FormSubmit size="large">{t('general:create')}</FormSubmit>
</Form>
)
}