From aecdb833c2e6f2c573bfc8c031e2934dadf5245b Mon Sep 17 00:00:00 2001 From: Vedanta Somnathe Date: Sun, 20 Apr 2025 13:58:25 -0500 Subject: [PATCH 1/7] preliminary approach for merging server errors --- .../next-server-actions/src/app/action.ts | 5 +-- .../src/app/client-component.tsx | 7 ---- .../src/app/shared-code.ts | 1 - packages/form-core/src/FormApi.ts | 40 +++++++++++++++++++ .../src/nextjs/createServerValidate.ts | 15 ++++++- 5 files changed, 56 insertions(+), 12 deletions(-) diff --git a/examples/react/next-server-actions/src/app/action.ts b/examples/react/next-server-actions/src/app/action.ts index 8ffa3fc02..2efca028b 100644 --- a/examples/react/next-server-actions/src/app/action.ts +++ b/examples/react/next-server-actions/src/app/action.ts @@ -9,9 +9,8 @@ import { formOpts } from './shared-code' const serverValidate = createServerValidate({ ...formOpts, onServerValidate: ({ value }) => { - if (value.age < 12) { - return 'Server validation: You must be at least 12 to sign up' - } + if (value.age < 12) + return { age: 'Server validation: You must be at least 12 to sign up' } }, }) 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 b7e62909c..a7104e555 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -3,7 +3,6 @@ import { useActionState } from 'react' import { mergeForm, useForm, useTransform } from '@tanstack/react-form' import { initialFormState } from '@tanstack/react-form/nextjs' -import { useStore } from '@tanstack/react-store' import someAction from './action' import { formOpts } from './shared-code' @@ -18,14 +17,8 @@ export const ClientComp = () => { ), }) - const formErrors = useStore(form.store, (formState) => formState.errors) - return (
form.handleSubmit()}> - {formErrors.map((error) => ( -

{error}

- ))} - { + 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 @@ -1388,6 +1413,20 @@ export class FormApi< }, })) } + + /** + * when we have an error for onServer in the state, we want + * to clear the error as soon as the user changes the value in the field + */ + if (cause !== 'server') { + this.baseStore.setState((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + onServer: undefined, + }, + })) + } }) return { hasErrored, fieldsErrorMap: currentValidationErrorMap } @@ -1774,6 +1813,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 adaa4de1f..2b2934494 100644 --- a/packages/react-form/src/nextjs/createServerValidate.ts +++ b/packages/react-form/src/nextjs/createServerValidate.ts @@ -104,12 +104,25 @@ export const createServerValidate = : onServerError ) as UnwrapFormAsyncValidateOrFn + // Extract string values from errors if they're in object format + const errorsArray = onServerErrorVal + ? Array.isArray(onServerErrorVal) + ? onServerErrorVal.map((err) => + typeof err === 'object' ? Object.values(err)[0] : err, + ) + : [ + typeof onServerErrorVal === 'object' + ? Object.values(onServerErrorVal)[0] + : onServerErrorVal, + ] + : [] + const formState: ServerFormState = { errorMap: { onServer: onServerError, }, values, - errors: onServerErrorVal ? [onServerErrorVal] : [], + errors: errorsArray, } throw new ServerValidateError({ From a9d48c9c570c4200991425af92fa46b7f282ed97 Mon Sep 17 00:00:00 2001 From: Vedanta Somnathe Date: Wed, 7 May 2025 12:42:22 +0530 Subject: [PATCH 2/7] server map checked for empty value --- packages/form-core/src/FormApi.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 0ca35301a..06cf8c527 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1022,7 +1022,11 @@ export class FormApi< state = newObj.state this.prevTransformArray = transformArray - const serverErrorMap = this.store.state.errorMap['onServer'] as any + const serverErrorMap = this.store.state.errorMap['onServer'] as + | Record + | undefined + + if (!serverErrorMap) return state batch(() => { void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( From 9413ca6961e52e382272945336be99f5ece5295a Mon Sep 17 00:00:00 2001 From: Vedanta Somnathe Date: Mon, 30 Jun 2025 17:07:51 +0530 Subject: [PATCH 3/7] using the global error type --- .../next-server-actions/src/app/action.ts | 7 ++++++- .../src/nextjs/createServerValidate.ts | 20 +++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/examples/react/next-server-actions/src/app/action.ts b/examples/react/next-server-actions/src/app/action.ts index 2efca028b..03154ec75 100644 --- a/examples/react/next-server-actions/src/app/action.ts +++ b/examples/react/next-server-actions/src/app/action.ts @@ -10,7 +10,12 @@ const serverValidate = createServerValidate({ ...formOpts, onServerValidate: ({ value }) => { if (value.age < 12) - return { age: 'Server validation: You must be at least 12 to sign up' } + return { + fields: { + age: 'Server validation: You must be at least 12 to sign up', + }, + form: 'Form level Error: Age must be at least 12', + } }, }) diff --git a/packages/react-form/src/nextjs/createServerValidate.ts b/packages/react-form/src/nextjs/createServerValidate.ts index 2b2934494..fc1bdaaae 100644 --- a/packages/react-form/src/nextjs/createServerValidate.ts +++ b/packages/react-form/src/nextjs/createServerValidate.ts @@ -96,13 +96,21 @@ export const createServerValidate = validationSource: 'form', })) as UnwrapFormAsyncValidateOrFn | undefined + console.log('ON SERVER ERROR', onServerError) + if (!onServerError) return values - const onServerErrorVal = ( - isGlobalFormValidationError(onServerError) - ? onServerError.form - : onServerError - ) as UnwrapFormAsyncValidateOrFn + let onServerErrorVal = undefined + let onServerErrorValFields = undefined + + 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 const errorsArray = onServerErrorVal @@ -119,7 +127,7 @@ export const createServerValidate = const formState: ServerFormState = { errorMap: { - onServer: onServerError, + onServer: onServerErrorValFields, }, values, errors: errorsArray, From 7c221792dc8ca5e085a96fe2a4439e066fe35f86 Mon Sep 17 00:00:00 2001 From: Vedanta Somnathe Date: Mon, 27 Oct 2025 20:29:54 -0700 Subject: [PATCH 4/7] removed console.log + changed to if statements --- examples/react/next-server-actions/src/app/action.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/react/next-server-actions/src/app/action.ts b/examples/react/next-server-actions/src/app/action.ts index 03154ec75..1f8906713 100644 --- a/examples/react/next-server-actions/src/app/action.ts +++ b/examples/react/next-server-actions/src/app/action.ts @@ -12,9 +12,9 @@ const serverValidate = createServerValidate({ if (value.age < 12) return { fields: { - age: 'Server validation: You must be at least 12 to sign up', + age: 'Field level error: You must be at least 12 to sign up', }, - form: 'Form level Error: Age must be at least 12', + form: 'Form level error: Age must be at least 12', } }, }) From 3c82c9873e35977f8a967384e627f88cc450f383 Mon Sep 17 00:00:00 2001 From: Vedanta Somnathe Date: Mon, 27 Oct 2025 20:30:41 -0700 Subject: [PATCH 5/7] removed console.log + changed to if statements --- .../src/nextjs/createServerValidate.ts | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/react-form/src/nextjs/createServerValidate.ts b/packages/react-form/src/nextjs/createServerValidate.ts index fc1bdaaae..61df9f162 100644 --- a/packages/react-form/src/nextjs/createServerValidate.ts +++ b/packages/react-form/src/nextjs/createServerValidate.ts @@ -96,8 +96,6 @@ export const createServerValidate = validationSource: 'form', })) as UnwrapFormAsyncValidateOrFn | undefined - console.log('ON SERVER ERROR', onServerError) - if (!onServerError) return values let onServerErrorVal = undefined @@ -113,17 +111,24 @@ export const createServerValidate = } // Extract string values from errors if they're in object format - const errorsArray = onServerErrorVal - ? Array.isArray(onServerErrorVal) - ? onServerErrorVal.map((err) => - typeof err === 'object' ? Object.values(err)[0] : err, - ) - : [ - typeof onServerErrorVal === 'object' - ? Object.values(onServerErrorVal)[0] - : onServerErrorVal, - ] - : [] + let errorsArray: any[] = [] + if (onServerErrorVal) { + if (Array.isArray(onServerErrorVal)) { + errorsArray = onServerErrorVal.map((err) => { + if (typeof err === 'object') { + return Object.values(err)[0] + } else { + return err + } + }) + } else { + if (typeof onServerErrorVal === 'object') { + errorsArray = [Object.values(onServerErrorVal)[0]] + } else { + errorsArray = [onServerErrorVal] + } + } + } const formState: ServerFormState = { errorMap: { From c922835d903521e4889172e7bc5fbe57fdfdf8fe Mon Sep 17 00:00:00 2001 From: Vedanta Somnathe Date: Thu, 30 Oct 2025 13:06:56 -0500 Subject: [PATCH 6/7] added simplification of object.values()[0] -> simple fields object usage --- .../next-server-actions/src/app/action.ts | 9 +++-- .../src/app/client-component.tsx | 34 +++++++++++++++++++ .../src/app/shared-code.ts | 1 + .../src/nextjs/createServerValidate.ts | 16 +++------ 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/examples/react/next-server-actions/src/app/action.ts b/examples/react/next-server-actions/src/app/action.ts index 1f8906713..e372e3064 100644 --- a/examples/react/next-server-actions/src/app/action.ts +++ b/examples/react/next-server-actions/src/app/action.ts @@ -9,13 +9,18 @@ import { formOpts } from './shared-code' const serverValidate = createServerValidate({ ...formOpts, onServerValidate: ({ value }) => { - if (value.age < 12) + if (value.age < 12) { return { fields: { age: 'Field level error: You must be at least 12 to sign up', }, - form: 'Form level error: Age must be at least 12', + 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 5f205cae2..d9f81bf5c 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -3,6 +3,7 @@ import { useActionState } from 'react' import { mergeForm, useForm, useTransform } from '@tanstack/react-form' import { initialFormState } from '@tanstack/react-form/nextjs' +import { useStore } from '@tanstack/react-store' import someAction from './action' import { formOpts } from './shared-code' @@ -17,8 +18,14 @@ export const ClientComp = () => { ), }) + const formErrors = useStore(form.store, (formState) => formState.errors) + return ( form.handleSubmit()}> + {formErrors.map((error) => ( +

{error}

+ ))} + { ) }} + + + 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 e24483f85..acf80e361 100644 --- a/examples/react/next-server-actions/src/app/shared-code.ts +++ b/examples/react/next-server-actions/src/app/shared-code.ts @@ -3,5 +3,6 @@ import { formOptions } from '@tanstack/react-form/nextjs' export const formOpts = formOptions({ defaultValues: { age: 0, + score: 0, }, }) diff --git a/packages/react-form/src/nextjs/createServerValidate.ts b/packages/react-form/src/nextjs/createServerValidate.ts index f78b882ed..8976ffea7 100644 --- a/packages/react-form/src/nextjs/createServerValidate.ts +++ b/packages/react-form/src/nextjs/createServerValidate.ts @@ -108,7 +108,9 @@ export const createServerValidate = 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 @@ -123,19 +125,9 @@ export const createServerValidate = if (onServerErrorVal) { if (Array.isArray(onServerErrorVal)) { errorsArray = onServerErrorVal.map((err) => { - if (typeof err === 'object') { - return Object.values(err)[0] - } else { - return err - } + return err }) - } else { - if (typeof onServerErrorVal === 'object') { - errorsArray = [Object.values(onServerErrorVal)[0]] - } else { - errorsArray = [onServerErrorVal] - } - } + } else errorsArray = [onServerErrorVal] } const formState: ServerFormState = { From d06c1ff272160e61ad5fc74a6bae370394d32f6a Mon Sep 17 00:00:00 2001 From: Vedanta Somnathe Date: Fri, 31 Oct 2025 16:14:22 -0500 Subject: [PATCH 7/7] remix support --- .../react/remix/app/routes/_index/route.tsx | 39 ++++++++++++++++++- .../src/remix/createServerValidate.ts | 32 +++++++++++---- 2 files changed, 62 insertions(+), 9 deletions(-) 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/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({