diff --git a/examples/react/next-server-actions/src/app/action.ts b/examples/react/next-server-actions/src/app/action.ts
index 8ffa3fc02..e372e3064 100644
--- a/examples/react/next-server-actions/src/app/action.ts
+++ b/examples/react/next-server-actions/src/app/action.ts
@@ -10,7 +10,16 @@ const serverValidate = createServerValidate({
...formOpts,
onServerValidate: ({ value }) => {
if (value.age < 12) {
- return 'Server validation: You must be at least 12 to sign up'
+ return {
+ fields: {
+ age: 'Field level error: You must be at least 12 to sign up',
+ },
+ form: '', // Can be omitted or be a string for form-level errors
+ }
+ }
+
+ if (value.score <= 90) {
+ return 'Form level error: Score must be over 90' // Also valid; Form-level
}
},
})
diff --git a/examples/react/next-server-actions/src/app/client-component.tsx b/examples/react/next-server-actions/src/app/client-component.tsx
index 380455588..d9f81bf5c 100644
--- a/examples/react/next-server-actions/src/app/client-component.tsx
+++ b/examples/react/next-server-actions/src/app/client-component.tsx
@@ -49,6 +49,33 @@ export const ClientComp = () => {
)
}}
+
+
+ value <= 80
+ ? 'Client validation: Score must be over 80'
+ : undefined,
+ }}
+ >
+ {(field) => {
+ return (
+
+
field.handleChange(e.target.valueAsNumber)}
+ />
+ {field.state.meta.errors.map((error) => (
+
{error}
+ ))}
+
+ )
+ }}
+
+
[formState.canSubmit, formState.isSubmitting]}
>
diff --git a/examples/react/next-server-actions/src/app/shared-code.ts b/examples/react/next-server-actions/src/app/shared-code.ts
index 31d9980f3..acf80e361 100644
--- a/examples/react/next-server-actions/src/app/shared-code.ts
+++ b/examples/react/next-server-actions/src/app/shared-code.ts
@@ -2,7 +2,7 @@ import { formOptions } from '@tanstack/react-form/nextjs'
export const formOpts = formOptions({
defaultValues: {
- firstName: '',
age: 0,
+ score: 0,
},
})
diff --git a/examples/react/remix/app/routes/_index/route.tsx b/examples/react/remix/app/routes/_index/route.tsx
index 81764938c..9afeea417 100644
--- a/examples/react/remix/app/routes/_index/route.tsx
+++ b/examples/react/remix/app/routes/_index/route.tsx
@@ -15,6 +15,7 @@ const formOpts = formOptions({
defaultValues: {
firstName: '',
age: 0,
+ score: 0,
},
})
@@ -22,7 +23,16 @@ const serverValidate = createServerValidate({
...formOpts,
onServerValidate: ({ value }) => {
if (value.age < 12) {
- return 'Server validation: You must be at least 12 to sign up'
+ return {
+ fields: {
+ age: 'Field level error: You must be at least 12 to sign up',
+ },
+ form: 'Form level error: You must be at least 12 to sign up', // Can be omitted or be a string for form-level errors
+ }
+ }
+
+ if (value.score <= 90) {
+ return 'Form level error: Score must be over 90' // Also valid; Form-level
}
},
})
@@ -79,7 +89,32 @@ export default function Index() {
return (
field.handleChange(e.target.valueAsNumber)}
+ />
+ {field.state.meta.errors.map((error) => (
+
{error}
+ ))}
+
+ )
+ }}
+
+
+ value < 80
+ ? 'Client validation: You must be at least 80'
+ : undefined,
+ }}
+ >
+ {(field) => {
+ return (
+
+
field.handleChange(e.target.valueAsNumber)}
diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts
index 926be4eef..8954ffb7b 100644
--- a/packages/form-core/src/FormApi.ts
+++ b/packages/form-core/src/FormApi.ts
@@ -1287,6 +1287,35 @@ export class FormApi<
this.options.transform?.fn(newObj)
state = newObj.state
this.prevTransformArray = transformArray
+
+ const serverErrorMap = this.store.state.errorMap['onServer'] as
+ | Record
+ | undefined
+
+ if (!serverErrorMap) return state
+
+ batch(() => {
+ void (Object.values(this.fieldInfo) as FieldInfo[]).forEach(
+ (field) => {
+ const fieldInstance = field.instance
+
+ if (!fieldInstance) return
+
+ if (fieldInstance.name in serverErrorMap) {
+ fieldInstance.setMeta((prev) => ({
+ ...prev,
+ errorMap: {
+ ...prev.errorMap,
+ onServer: serverErrorMap[fieldInstance.name],
+ },
+ }))
+ fieldInstance.mount()
+ }
+
+ this.validateField(fieldInstance.name, 'server')
+ },
+ )
+ })
}
return state
@@ -2239,6 +2268,7 @@ export class FormApi<
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
...prev?.errorMap,
onMount: undefined,
+ onServer: undefined,
},
}))
}
diff --git a/packages/react-form/src/nextjs/createServerValidate.ts b/packages/react-form/src/nextjs/createServerValidate.ts
index 903a6f520..8976ffea7 100644
--- a/packages/react-form/src/nextjs/createServerValidate.ts
+++ b/packages/react-form/src/nextjs/createServerValidate.ts
@@ -106,18 +106,36 @@ export const createServerValidate =
if (!onServerError) return values
- const onServerErrorVal = (
- isGlobalFormValidationError(onServerError)
- ? onServerError.form
- : onServerError
- ) as UnwrapFormAsyncValidateOrFn
+ let onServerErrorVal = undefined
+ let onServerErrorValFields = undefined
+ // ^ if fields is defined, this object is { fieldName: "Error Message" }
+
+ // checks if it's an object with 'fields'
+ if (isGlobalFormValidationError(onServerError)) {
+ onServerErrorVal =
+ onServerError.form as UnwrapFormAsyncValidateOrFn
+ onServerErrorValFields =
+ onServerError.fields as UnwrapFormAsyncValidateOrFn
+ } else {
+ onServerErrorVal = onServerError as UnwrapFormAsyncValidateOrFn
+ }
+
+ // Extract string values from errors if they're in object format
+ let errorsArray: any[] = []
+ if (onServerErrorVal) {
+ if (Array.isArray(onServerErrorVal)) {
+ errorsArray = onServerErrorVal.map((err) => {
+ return err
+ })
+ } else errorsArray = [onServerErrorVal]
+ }
const formState: ServerFormState = {
errorMap: {
- onServer: onServerError,
+ onServer: onServerErrorValFields,
},
values,
- errors: onServerErrorVal ? [onServerErrorVal] : [],
+ errors: errorsArray,
}
throw new ServerValidateError({
diff --git a/packages/react-form/src/remix/createServerValidate.ts b/packages/react-form/src/remix/createServerValidate.ts
index 903a6f520..8976ffea7 100644
--- a/packages/react-form/src/remix/createServerValidate.ts
+++ b/packages/react-form/src/remix/createServerValidate.ts
@@ -106,18 +106,36 @@ export const createServerValidate =
if (!onServerError) return values
- const onServerErrorVal = (
- isGlobalFormValidationError(onServerError)
- ? onServerError.form
- : onServerError
- ) as UnwrapFormAsyncValidateOrFn
+ let onServerErrorVal = undefined
+ let onServerErrorValFields = undefined
+ // ^ if fields is defined, this object is { fieldName: "Error Message" }
+
+ // checks if it's an object with 'fields'
+ if (isGlobalFormValidationError(onServerError)) {
+ onServerErrorVal =
+ onServerError.form as UnwrapFormAsyncValidateOrFn
+ onServerErrorValFields =
+ onServerError.fields as UnwrapFormAsyncValidateOrFn
+ } else {
+ onServerErrorVal = onServerError as UnwrapFormAsyncValidateOrFn
+ }
+
+ // Extract string values from errors if they're in object format
+ let errorsArray: any[] = []
+ if (onServerErrorVal) {
+ if (Array.isArray(onServerErrorVal)) {
+ errorsArray = onServerErrorVal.map((err) => {
+ return err
+ })
+ } else errorsArray = [onServerErrorVal]
+ }
const formState: ServerFormState = {
errorMap: {
- onServer: onServerError,
+ onServer: onServerErrorValFields,
},
values,
- errors: onServerErrorVal ? [onServerErrorVal] : [],
+ errors: errorsArray,
}
throw new ServerValidateError({