Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: skips field validations until the form is submitted #10580

Merged
merged 8 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions examples/multi-tenant/src/app/(payload)/admin/importMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { TenantSelector as TenantSelector_d6d5f193a167989e2ee7d14202901e62 } fro
import { TenantSelectionProvider as TenantSelectionProvider_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'

export const importMap = {
"@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a,
"@payloadcms/plugin-multi-tenant/rsc#TenantSelector": TenantSelector_d6d5f193a167989e2ee7d14202901e62,
"@payloadcms/plugin-multi-tenant/client#TenantSelectionProvider": TenantSelectionProvider_1d0591e3cf4f332c83a86da13a0de59a
'@payloadcms/plugin-multi-tenant/client#TenantField':
TenantField_1d0591e3cf4f332c83a86da13a0de59a,
'@payloadcms/plugin-multi-tenant/rsc#TenantSelector':
TenantSelector_d6d5f193a167989e2ee7d14202901e62,
'@payloadcms/plugin-multi-tenant/client#TenantSelectionProvider':
TenantSelectionProvider_1d0591e3cf4f332c83a86da13a0de59a,
}
1 change: 1 addition & 0 deletions packages/next/src/views/Account/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const Account: React.FC<AdminViewProps> = async ({
renderAllFields: true,
req,
schemaPath: collectionConfig.slug,
skipValidation: true,
})

// Fetch document lock state
Expand Down
3 changes: 2 additions & 1 deletion packages/next/src/views/CreateFirstUser/index.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const CreateFirstUserClient: React.FC<{
const collectionConfig = getEntityConfig({ collectionSlug: userSlug })

const onChange: FormProps['onChange'][0] = React.useCallback(
async ({ formState: prevFormState }) => {
async ({ formState: prevFormState, submitted }) => {
const controller = handleAbortRef(abortOnChangeRef)

const response = await getFormState({
Expand All @@ -58,6 +58,7 @@ export const CreateFirstUserClient: React.FC<{
operation: 'create',
schemaPath: userSlug,
signal: controller.signal,
skipValidation: !submitted,
})

abortOnChangeRef.current = null
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/views/CreateFirstUser/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const CreateFirstUserView: React.FC<AdminViewProps> = async ({ initPageRe
renderAllFields: true,
req,
schemaPath: collectionConfig.slug,
skipValidation: true,
})

return (
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/views/Document/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export const renderDocument = async ({
renderAllFields: true,
req,
schemaPath: collectionSlug || globalSlug,
skipValidation: true,
}),
])

Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/views/LivePreview/index.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ const PreviewView: React.FC<Props> = ({
returnLockStatus: false,
schemaPath: entitySlug,
signal: controller.signal,
skipValidation: true,
})

// Unlock the document after save
Expand Down Expand Up @@ -267,7 +268,7 @@ const PreviewView: React.FC<Props> = ({
)

const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => {
async ({ formState: prevFormState, submitted }) => {
const controller = handleAbortRef(abortOnChangeRef)

const currentTime = Date.now()
Expand All @@ -292,6 +293,7 @@ const PreviewView: React.FC<Props> = ({
returnLockStatus: isLockingEnabled ? true : false,
schemaPath,
signal: controller.signal,
skipValidation: !submitted,
updateLastEdited,
})

Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/admin/forms/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export type BuildFormStateArgs = {
req: PayloadRequest
returnLockStatus?: boolean
schemaPath: string
skipValidation?: boolean
updateLastEdited?: boolean
} & (
| {
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/elements/BulkUpload/EditForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function EditForm({ submitted }: EditFormProps) {
)

const onChange: NonNullable<FormProps['onChange']>[0] = useCallback(
async ({ formState: prevFormState }) => {
async ({ formState: prevFormState, submitted }) => {
const controller = handleAbortRef(abortOnChangeRef)

const docPreferences = await getDocPreferences()
Expand All @@ -121,6 +121,7 @@ export function EditForm({ submitted }: EditFormProps) {
operation: 'create',
schemaPath,
signal: controller.signal,
skipValidation: !submitted,
})

abortOnChangeRef.current = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
operation: 'create',
renderAllFields: true,
schemaPath: collectionSlug,
skipValidation: true,
})
initialStateRef.current = formStateWithoutFiles
setHasInitializedState(true)
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/elements/EditMany/DrawerContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export const EditManyDrawerContent: React.FC<
operation: 'update',
schemaPath: slug,
signal: controller.signal,
skipValidation: true,
})

setInitialState(result)
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/src/forms/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ export const Form: React.FC<FormProps> = (props) => {
renderAllFields: true,
schemaPath: collectionSlug ? collectionSlug : globalSlug,
signal: controller.signal,
skipValidation: true,
})

contextRef.current = { ...initContextState } as FormContextType
Expand Down Expand Up @@ -665,6 +666,7 @@ export const Form: React.FC<FormProps> = (props) => {
// Edit view default onChange is in packages/ui/src/views/Edit/index.tsx. This onChange usually sends a form state request
revalidatedFormState = await onChangeFn({
formState: deepCopyObjectSimpleWithoutReactComponents(contextRef.current.fields),
submitted,
})
}

Expand Down Expand Up @@ -699,7 +701,7 @@ export const Form: React.FC<FormProps> = (props) => {
`fields` updates before `modified`, because setModified is in a setTimeout.
So on the first change, modified is false, so we don't trigger the effect even though we should.
**/
[contextRef.current.fields, modified],
[contextRef.current.fields, modified, submitted],
[dispatchFields, onChange],
{
delay: 250,
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/forms/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export type FormProps = {
initialState?: FormState
isInitializing?: boolean
log?: boolean
onChange?: ((args: { formState: FormState }) => Promise<FormState>)[]
onChange?: ((args: { formState: FormState; submitted?: boolean }) => Promise<FormState>)[]
onSubmit?: (fields: FormState, data: Data) => void
onSuccess?: (json: unknown) => Promise<FormState | void> | void
redirect?: string
Expand Down
40 changes: 20 additions & 20 deletions packages/ui/src/forms/fieldSchemasToFormState/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,34 +47,33 @@ type Args = {
renderAllFields: boolean
renderFieldFn?: RenderFieldMethod
req: PayloadRequest

schemaPath: string
skipValidation?: boolean
}

export const fieldSchemasToFormState = async (args: Args): Promise<FormState> => {
if (!args.clientFieldSchemaMap && args.renderFieldFn) {
export const fieldSchemasToFormState = async ({
id,
clientFieldSchemaMap,
collectionSlug,
data = {},
fields,
fieldSchemaMap,
operation,
permissions,
preferences,
previousFormState,
renderAllFields,
renderFieldFn,
req,
schemaPath,
skipValidation,
}: Args): Promise<FormState> => {
if (!clientFieldSchemaMap && renderFieldFn) {
console.warn(
'clientFieldSchemaMap is not passed to fieldSchemasToFormState - this will reduce performance',
)
}

const {
id,
clientFieldSchemaMap,
collectionSlug,
data = {},
fields,
fieldSchemaMap,
operation,
permissions,
preferences,
previousFormState,
renderAllFields,
renderFieldFn,
req,
schemaPath,
} = args

if (fields && fields.length) {
const state: FormStateWithoutComponents = {}

Expand Down Expand Up @@ -110,6 +109,7 @@ export const fieldSchemasToFormState = async (args: Args): Promise<FormState> =>
renderAllFields,
renderFieldFn,
req,
skipValidation,
state,
})

Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/utilities/buildFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const buildFormState = async (
},
returnLockStatus,
schemaPath = collectionSlug || globalSlug,
skipValidation,
updateLastEdited,
} = args

Expand Down Expand Up @@ -194,6 +195,7 @@ export const buildFormState = async (
renderFieldFn: renderField,
req,
schemaPath,
skipValidation,
})

// Maintain form state of auth / upload fields
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/src/views/Edit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
returnLockStatus: false,
schemaPath: schemaPathSegments.join('.'),
signal: controller.signal,
skipValidation: true,
})

// Unlock the document after save
Expand Down Expand Up @@ -323,7 +324,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
)

const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => {
async ({ formState: prevFormState, submitted }) => {
const controller = handleAbortRef(abortOnChangeRef)

const currentTime = Date.now()
Expand All @@ -345,6 +346,7 @@ export const DefaultEditView: React.FC<ClientSideEditViewProps> = ({
formState: prevFormState,
globalSlug,
operation,
skipValidation: !submitted,
// Performance optimization: Setting it to false ensure that only fields that have explicit requireRender set in the form state will be rendered (e.g. new array rows).
// We only want to render ALL fields on initial render, not in onChange.
renderAllFields: false,
Expand Down