From 9141330006cdecf604c40940bb440b966829fec3 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 16:39:14 -0800 Subject: [PATCH 1/5] chore: naive migrate to new store package --- packages/form-core/package.json | 2 +- packages/form-core/src/FieldApi.ts | 122 +++-- packages/form-core/src/FormApi.ts | 833 +++++++++++++++-------------- pnpm-lock.yaml | 78 ++- 4 files changed, 553 insertions(+), 482 deletions(-) diff --git a/packages/form-core/package.json b/packages/form-core/package.json index 3bad999d0..645b14fc1 100644 --- a/packages/form-core/package.json +++ b/packages/form-core/package.json @@ -53,7 +53,7 @@ "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/pacer-lite": "^0.1.1", - "@tanstack/store": "^0.7.7" + "@tanstack/store": "https://pkg.pr.new/@tanstack/store@265" }, "devDependencies": { "arktype": "^2.1.22", diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 1d5ba3096..6cad062a1 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1,4 +1,4 @@ -import { Derived, batch } from '@tanstack/store' +import { createStore, type Store } from '@tanstack/store' import { isStandardSchemaValidator, standardSchemaValidators, @@ -1090,7 +1090,7 @@ export class FieldApi< /** * The field state store. */ - store!: Derived< + store!: Store< FieldState< TParentData, TName, @@ -1167,10 +1167,9 @@ export class FieldApi< formListeners: {} as Record, } - this.store = new Derived({ - deps: [this.form.store], - fn: ({ prevVal: _prevVal }) => { - const prevVal = _prevVal as + this.store = createStore( + ( + prevVal: | FieldState< TParentData, TName, @@ -1194,7 +1193,10 @@ export class FieldApi< TFormOnDynamic, TFormOnDynamicAsync > - | undefined + | undefined, + ) => { + // Temp hack to subscribe to form.store + this.form.store.get() const meta = this.form.getFieldMeta(this.name) ?? { ...defaultFieldMeta, @@ -1242,7 +1244,7 @@ export class FieldApi< TFormOnDynamicAsync > }, - }) + ) } /** @@ -1275,8 +1277,6 @@ export class FieldApi< * Mounts the field instance to the form. */ mount = () => { - const cleanup = this.store.mount() - if (this.options.defaultValue !== undefined && !this.getMeta().isTouched) { this.form.setFieldValue(this.name, this.options.defaultValue, { dontUpdateMeta: true, @@ -1321,8 +1321,6 @@ export class FieldApi< value: this.state.value, fieldApi: this, }) - - return cleanup } /** @@ -1623,62 +1621,62 @@ export class FieldApi< // Needs type cast as eslint errantly believes this is always falsy let hasErrored = false as boolean - batch(() => { - const validateFieldFn = ( - field: AnyFieldApi, - validateObj: SyncValidator, - ) => { - const errorMapKey = getErrorMapKey(validateObj.cause) - - const fieldLevelError = validateObj.validate - ? normalizeError( - field.runValidator({ - validate: validateObj.validate, - value: { - value: field.store.state.value, - validationSource: 'field', - fieldApi: field, - }, - type: 'validate', - }), - ) - : undefined + // batch(() => { + const validateFieldFn = ( + field: AnyFieldApi, + validateObj: SyncValidator, + ) => { + const errorMapKey = getErrorMapKey(validateObj.cause) - const formLevelError = errorFromForm[errorMapKey] + const fieldLevelError = validateObj.validate + ? normalizeError( + field.runValidator({ + validate: validateObj.validate, + value: { + value: field.store.state.value, + validationSource: 'field', + fieldApi: field, + }, + type: 'validate', + }), + ) + : undefined - const { newErrorValue, newSource } = - determineFieldLevelErrorSourceAndValue({ - formLevelError, - fieldLevelError, - }) + const formLevelError = errorFromForm[errorMapKey] - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (field.state.meta.errorMap?.[errorMapKey] !== newErrorValue) { - field.setMeta((prev) => ({ - ...prev, - errorMap: { - ...prev.errorMap, - [errorMapKey]: newErrorValue, - }, - errorSourceMap: { - ...prev.errorSourceMap, - [errorMapKey]: newSource, - }, - })) - } - if (newErrorValue) { - hasErrored = true - } - } + const { newErrorValue, newSource } = + determineFieldLevelErrorSourceAndValue({ + formLevelError, + fieldLevelError, + }) - for (const validateObj of validates) { - validateFieldFn(this, validateObj) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (field.state.meta.errorMap?.[errorMapKey] !== newErrorValue) { + field.setMeta((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + [errorMapKey]: newErrorValue, + }, + errorSourceMap: { + ...prev.errorSourceMap, + [errorMapKey]: newSource, + }, + })) } - for (const fieldValitateObj of linkedFieldValidates) { - if (!fieldValitateObj.validate) continue - validateFieldFn(fieldValitateObj.field, fieldValitateObj) + if (newErrorValue) { + hasErrored = true } - }) + } + + for (const validateObj of validates) { + validateFieldFn(this, validateObj) + } + for (const fieldValitateObj of linkedFieldValidates) { + if (!fieldValitateObj.validate) continue + validateFieldFn(fieldValitateObj.field, fieldValitateObj) + } + // }) /** * when we have an error for onSubmit in the state, we want diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index e7e9fb2ad..30ff27b35 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1,4 +1,4 @@ -import { Derived, Store, batch } from '@tanstack/store' +import { createStore, type Store } from '@tanstack/store' import { deleteBy, determineFormLevelErrorSourceAndValue, @@ -931,7 +931,7 @@ export class FormApi< TOnServer > > - fieldMetaDerived: Derived< + fieldMetaDerived: Store< FormState< TFormData, TOnMount, @@ -946,7 +946,7 @@ export class FormApi< TOnServer >['fieldMeta'] > - store: Derived< + store: Store< FormState< TFormData, TOnMount, @@ -1021,22 +1021,33 @@ export class FormApi< this._devtoolsSubmissionOverride = false - this.baseStore = new Store( + this.baseStore = createStore( getDefaultFormState({ ...(opts?.defaultState as any), values: opts?.defaultValues ?? opts?.defaultState?.values, isFormValid: true, }), - ) + ) as never + + let prevBaseStore: + | BaseFormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + > + | undefined = undefined - this.fieldMetaDerived = new Derived({ - deps: [this.baseStore], - fn: ({ prevDepVals, currDepVals, prevVal: _prevVal }) => { - const prevVal = _prevVal as - | Record, AnyFieldMeta> - | undefined - const prevBaseStore = prevDepVals?.[0] - const currBaseStore = currDepVals[0] + this.fieldMetaDerived = createStore( + (prevVal: Record, AnyFieldMeta> | undefined) => { + const currBaseStore = this.baseStore.get() let originalMetaCount = 0 @@ -1133,149 +1144,14 @@ export class FormApi< return prevVal } + prevBaseStore = this.baseStore.get() + return fieldMeta }, - }) + ) as never - this.store = new Derived({ - deps: [this.baseStore, this.fieldMetaDerived], - fn: ({ prevDepVals, currDepVals, prevVal: _prevVal }) => { - const prevVal = _prevVal as - | FormState< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer - > - | undefined - const prevBaseStore = prevDepVals?.[0] - const currBaseStore = currDepVals[0] - const currFieldMeta = currDepVals[1] - - // Computed state - const fieldMetaValues = Object.values(currFieldMeta).filter( - Boolean, - ) as AnyFieldMeta[] - - const isFieldsValidating = fieldMetaValues.some( - (field) => field.isValidating, - ) - - const isFieldsValid = fieldMetaValues.every((field) => field.isValid) - - const isTouched = fieldMetaValues.some((field) => field.isTouched) - const isBlurred = fieldMetaValues.some((field) => field.isBlurred) - const isDefaultValue = fieldMetaValues.every( - (field) => field.isDefaultValue, - ) - - const shouldInvalidateOnMount = - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - isTouched && currBaseStore.errorMap?.onMount - - const isDirty = fieldMetaValues.some((field) => field.isDirty) - const isPristine = !isDirty - - const hasOnMountError = Boolean( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - currBaseStore.errorMap?.onMount || - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - fieldMetaValues.some((f) => f?.errorMap?.onMount), - ) - - const isValidating = !!isFieldsValidating - - // As `errors` is not a primitive, we need to aggressively persist the same referencial value for performance reasons - let errors = prevVal?.errors ?? [] - if ( - !prevBaseStore || - currBaseStore.errorMap !== prevBaseStore.errorMap - ) { - errors = Object.values(currBaseStore.errorMap).reduce< - Array< - | UnwrapFormValidateOrFn - | UnwrapFormValidateOrFn - | UnwrapFormAsyncValidateOrFn - | UnwrapFormValidateOrFn - | UnwrapFormAsyncValidateOrFn - | UnwrapFormValidateOrFn - | UnwrapFormAsyncValidateOrFn - | UnwrapFormAsyncValidateOrFn - > - >((prev, curr) => { - if (curr === undefined) return prev - - if (curr && isGlobalFormValidationError(curr)) { - prev.push(curr.form as never) - return prev - } - prev.push(curr as never) - return prev - }, []) - } - - const isFormValid = errors.length === 0 - const isValid = isFieldsValid && isFormValid - const submitInvalid = this.options.canSubmitWhenInvalid ?? false - const canSubmit = - (currBaseStore.submissionAttempts === 0 && - !isTouched && - !hasOnMountError) || - (!isValidating && !currBaseStore.isSubmitting && isValid) || - submitInvalid - - let errorMap = currBaseStore.errorMap - if (shouldInvalidateOnMount) { - errors = errors.filter( - (err) => err !== currBaseStore.errorMap.onMount, - ) - errorMap = Object.assign(errorMap, { onMount: undefined }) - } - - if ( - prevVal && - prevBaseStore && - prevVal.errorMap === errorMap && - prevVal.fieldMeta === this.fieldMetaDerived.state && - prevVal.errors === errors && - prevVal.isFieldsValidating === isFieldsValidating && - prevVal.isFieldsValid === isFieldsValid && - prevVal.isFormValid === isFormValid && - prevVal.isValid === isValid && - prevVal.canSubmit === canSubmit && - prevVal.isTouched === isTouched && - prevVal.isBlurred === isBlurred && - prevVal.isPristine === isPristine && - prevVal.isDefaultValue === isDefaultValue && - prevVal.isDirty === isDirty && - evaluate(prevBaseStore, currBaseStore) - ) { - return prevVal - } - - let state = { - ...currBaseStore, - errorMap, - fieldMeta: this.fieldMetaDerived.state, - errors, - isFieldsValidating, - isFieldsValid, - isFormValid, - isValid, - canSubmit, - isTouched, - isBlurred, - isPristine, - isDefaultValue, - isDirty, - } as FormState< + let prevBaseStoreForStore: + | BaseFormState< TFormData, TOnMount, TOnChange, @@ -1288,23 +1164,171 @@ export class FormApi< TOnDynamicAsync, TOnServer > + | undefined = undefined - // Only run transform if state has shallowly changed - IE how React.useEffect works - const transformArray = this.options.transform?.deps ?? [] - const shouldTransform = - transformArray.length !== this.prevTransformArray.length || - transformArray.some((val, i) => val !== this.prevTransformArray[i]) - - if (shouldTransform) { - const newObj = Object.assign({}, this, { state }) - // This mutates the state - this.options.transform?.fn(newObj) - state = newObj.state - this.prevTransformArray = transformArray - } + this.store = createStore< + FormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + > + >((prevVal) => { + const currBaseStore = this.baseStore.get() + const currFieldMeta = this.fieldMetaDerived.get() - return state - }, + // Computed state + const fieldMetaValues = Object.values(currFieldMeta).filter( + Boolean, + ) as AnyFieldMeta[] + + const isFieldsValidating = fieldMetaValues.some( + (field) => field.isValidating, + ) + + const isFieldsValid = fieldMetaValues.every((field) => field.isValid) + + const isTouched = fieldMetaValues.some((field) => field.isTouched) + const isBlurred = fieldMetaValues.some((field) => field.isBlurred) + const isDefaultValue = fieldMetaValues.every( + (field) => field.isDefaultValue, + ) + + const shouldInvalidateOnMount = + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + isTouched && currBaseStore.errorMap?.onMount + + const isDirty = fieldMetaValues.some((field) => field.isDirty) + const isPristine = !isDirty + + const hasOnMountError = Boolean( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + currBaseStore.errorMap?.onMount || + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + fieldMetaValues.some((f) => f?.errorMap?.onMount), + ) + + const isValidating = !!isFieldsValidating + + // As `errors` is not a primitive, we need to aggressively persist the same referencial value for performance reasons + let errors = prevVal?.errors ?? [] + if ( + !prevBaseStoreForStore || + currBaseStore.errorMap !== prevBaseStoreForStore.errorMap + ) { + errors = Object.values(currBaseStore.errorMap).reduce< + Array< + | UnwrapFormValidateOrFn + | UnwrapFormValidateOrFn + | UnwrapFormAsyncValidateOrFn + | UnwrapFormValidateOrFn + | UnwrapFormAsyncValidateOrFn + | UnwrapFormValidateOrFn + | UnwrapFormAsyncValidateOrFn + | UnwrapFormAsyncValidateOrFn + > + >((prev, curr) => { + if (curr === undefined) return prev + + if (curr && isGlobalFormValidationError(curr)) { + prev.push(curr.form as never) + return prev + } + prev.push(curr as never) + return prev + }, []) + } + + const isFormValid = errors.length === 0 + const isValid = isFieldsValid && isFormValid + const submitInvalid = this.options.canSubmitWhenInvalid ?? false + const canSubmit = + (currBaseStore.submissionAttempts === 0 && + !isTouched && + !hasOnMountError) || + (!isValidating && !currBaseStore.isSubmitting && isValid) || + submitInvalid + + let errorMap = currBaseStore.errorMap + if (shouldInvalidateOnMount) { + errors = errors.filter((err) => err !== currBaseStore.errorMap.onMount) + errorMap = Object.assign(errorMap, { onMount: undefined }) + } + + if ( + prevVal && + prevBaseStoreForStore && + prevVal.errorMap === errorMap && + prevVal.fieldMeta === this.fieldMetaDerived.state && + prevVal.errors === errors && + prevVal.isFieldsValidating === isFieldsValidating && + prevVal.isFieldsValid === isFieldsValid && + prevVal.isFormValid === isFormValid && + prevVal.isValid === isValid && + prevVal.canSubmit === canSubmit && + prevVal.isTouched === isTouched && + prevVal.isBlurred === isBlurred && + prevVal.isPristine === isPristine && + prevVal.isDefaultValue === isDefaultValue && + prevVal.isDirty === isDirty && + evaluate(prevBaseStoreForStore, currBaseStore) + ) { + return prevVal + } + + let state = { + ...currBaseStore, + errorMap, + fieldMeta: this.fieldMetaDerived.state, + errors, + isFieldsValidating, + isFieldsValid, + isFormValid, + isValid, + canSubmit, + isTouched, + isBlurred, + isPristine, + isDefaultValue, + isDirty, + } as FormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + > + + // Only run transform if state has shallowly changed - IE how React.useEffect works + const transformArray = this.options.transform?.deps ?? [] + const shouldTransform = + transformArray.length !== this.prevTransformArray.length || + transformArray.some((val, i) => val !== this.prevTransformArray[i]) + + if (shouldTransform) { + const newObj = Object.assign({}, this, { state }) + // This mutates the state + this.options.transform?.fn(newObj) + state = newObj.state + this.prevTransformArray = transformArray + } + + prevBaseStoreForStore = this.baseStore.get() + + return state }) this.handleSubmit = this.handleSubmit.bind(this) @@ -1342,9 +1366,6 @@ export class FormApi< } mount = () => { - const cleanupFieldMetaDerived = this.fieldMetaDerived.mount() - const cleanupStoreDerived = this.store.mount() - // devtool broadcasts const cleanupDevtoolBroadcast = this.store.subscribe(() => { throttleFormState(this) @@ -1388,9 +1409,7 @@ export class FormApi< cleanupFormForceSubmitListener() cleanupFormResetListener() cleanupFormStateListener() - cleanupDevtoolBroadcast() - cleanupFieldMetaDerived() - cleanupStoreDerived() + cleanupDevtoolBroadcast.unsubscribe() // broadcast form unmount for devtools formEventClient.emit('form-unmounted', { @@ -1459,28 +1478,28 @@ export class FormApi< if (!shouldUpdateValues && !shouldUpdateState && !shouldUpdateReeval) return - batch(() => { - this.baseStore.setState(() => - getDefaultFormState( - Object.assign( - {}, - this.state as any, - - shouldUpdateState ? options.defaultState : {}, - - shouldUpdateValues - ? { - values: options.defaultValues, - } - : {}, - - shouldUpdateReeval - ? { _force_re_eval: !this.state._force_re_eval } - : {}, - ), + // batch(() => { + this.baseStore.setState(() => + getDefaultFormState( + Object.assign( + {}, + this.state as any, + + shouldUpdateState ? options.defaultState : {}, + + shouldUpdateValues + ? { + values: options.defaultValues, + } + : {}, + + shouldUpdateReeval + ? { _force_re_eval: !this.state._force_re_eval } + : {}, ), - ) - }) + ), + ) + // }) formEventClient.emit('form-api', { id: this._formId, @@ -1524,26 +1543,26 @@ export class FormApi< */ validateAllFields = async (cause: ValidationCause) => { const fieldValidationPromises: Promise[] = [] as any - batch(() => { - void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( - (field) => { - if (!field.instance) return - const fieldInstance = field.instance - // Validate the field - fieldValidationPromises.push( - // Remember, `validate` is either a sync operation or a promise - Promise.resolve().then(() => - fieldInstance.validate(cause, { skipFormValidation: true }), - ), - ) - // If any fields are not touched - if (!field.instance.state.meta.isTouched) { - // Mark them as touched - field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) - } - }, - ) - }) + // batch(() => { + void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( + (field) => { + if (!field.instance) return + const fieldInstance = field.instance + // Validate the field + fieldValidationPromises.push( + // Remember, `validate` is either a sync operation or a promise + Promise.resolve().then(() => + fieldInstance.validate(cause, { skipFormValidation: true }), + ), + ) + // If any fields are not touched + if (!field.instance.state.meta.isTouched) { + // Mark them as touched + field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) + } + }, + ) + // }) const fieldErrorMapMap = await Promise.all(fieldValidationPromises) return fieldErrorMapMap.flat() @@ -1578,13 +1597,13 @@ export class FormApi< // Validate the fields const fieldValidationPromises: Promise[] = [] as any - batch(() => { - fieldsToValidate.forEach((nestedField) => { - fieldValidationPromises.push( - Promise.resolve().then(() => this.validateField(nestedField, cause)), - ) - }) + // batch(() => { + fieldsToValidate.forEach((nestedField) => { + fieldValidationPromises.push( + Promise.resolve().then(() => this.validateField(nestedField, cause)), + ) }) + // }) const fieldErrorMapMap = await Promise.all(fieldValidationPromises) return fieldErrorMapMap.flat() @@ -1664,136 +1683,136 @@ export class FormApi< TOnDynamicAsync > = {} - batch(() => { - for (const validateObj of validates) { - if (!validateObj.validate) continue + // batch(() => { + for (const validateObj of validates) { + if (!validateObj.validate) continue - const rawError = this.runValidator({ - validate: validateObj.validate, - value: { - value: this.state.values, - formApi: this, - validationSource: 'form', - }, - type: 'validate', - }) + const rawError = this.runValidator({ + validate: validateObj.validate, + value: { + value: this.state.values, + formApi: this, + validationSource: 'form', + }, + type: 'validate', + }) - const { formError, fieldErrors } = normalizeError(rawError) + const { formError, fieldErrors } = normalizeError(rawError) - const errorMapKey = getErrorMapKey(validateObj.cause) + const errorMapKey = getErrorMapKey(validateObj.cause) - const allFieldsToProcess = new Set([ - ...Object.keys(this.state.fieldMeta), - ...Object.keys(fieldErrors || {}), - ] as DeepKeys[]) + const allFieldsToProcess = new Set([ + ...Object.keys(this.state.fieldMeta), + ...Object.keys(fieldErrors || {}), + ] as DeepKeys[]) - for (const field of allFieldsToProcess) { - if ( - this.baseStore.state.fieldMetaBase[field] === undefined && - !fieldErrors?.[field] - ) { - continue - } + for (const field of allFieldsToProcess) { + if ( + this.baseStore.state.fieldMetaBase[field] === undefined && + !fieldErrors?.[field] + ) { + continue + } - const fieldMeta = this.getFieldMeta(field) ?? defaultFieldMeta - const { - errorMap: currentErrorMap, - errorSourceMap: currentErrorMapSource, - } = fieldMeta + const fieldMeta = this.getFieldMeta(field) ?? defaultFieldMeta + const { + errorMap: currentErrorMap, + errorSourceMap: currentErrorMapSource, + } = fieldMeta - const newFormValidatorError = fieldErrors?.[field] + const newFormValidatorError = fieldErrors?.[field] - const { newErrorValue, newSource } = - determineFormLevelErrorSourceAndValue({ - newFormValidatorError, - isPreviousErrorFromFormValidator: - // These conditional checks are required, otherwise we get runtime errors. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - currentErrorMapSource?.[errorMapKey] === 'form', + const { newErrorValue, newSource } = + determineFormLevelErrorSourceAndValue({ + newFormValidatorError, + isPreviousErrorFromFormValidator: + // These conditional checks are required, otherwise we get runtime errors. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - previousErrorValue: currentErrorMap?.[errorMapKey], - }) - - if (newSource === 'form') { - currentValidationErrorMap[field] = { - ...currentValidationErrorMap[field], - [errorMapKey]: newFormValidatorError, - } - } + currentErrorMapSource?.[errorMapKey] === 'form', + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + previousErrorValue: currentErrorMap?.[errorMapKey], + }) - // This conditional check is required, otherwise we get runtime errors. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (currentErrorMap?.[errorMapKey] !== newErrorValue) { - this.setFieldMeta(field, (prev = defaultFieldMeta) => ({ - ...prev, - errorMap: { - ...prev.errorMap, - [errorMapKey]: newErrorValue, - }, - errorSourceMap: { - ...prev.errorSourceMap, - [errorMapKey]: newSource, - }, - })) + if (newSource === 'form') { + currentValidationErrorMap[field] = { + ...currentValidationErrorMap[field], + [errorMapKey]: newFormValidatorError, } } + // This conditional check is required, otherwise we get runtime errors. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.state.errorMap?.[errorMapKey] !== formError) { - this.baseStore.setState((prev) => ({ + if (currentErrorMap?.[errorMapKey] !== newErrorValue) { + this.setFieldMeta(field, (prev = defaultFieldMeta) => ({ ...prev, errorMap: { ...prev.errorMap, - [errorMapKey]: formError, + [errorMapKey]: newErrorValue, + }, + errorSourceMap: { + ...prev.errorSourceMap, + [errorMapKey]: newSource, }, })) } - - if (formError || fieldErrors) { - hasErrored = true - } } - /** - * when we have an error for onSubmit in the state, we want - * to clear the error as soon as the user enters a valid value in the field - */ - const submitErrKey = getErrorMapKey('submit') - if ( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.state.errorMap?.[submitErrKey] && - cause !== 'submit' && - !hasErrored - ) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.state.errorMap?.[errorMapKey] !== formError) { this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, - [submitErrKey]: undefined, + [errorMapKey]: formError, }, })) } - /** - * when we have an error for onServer in the state, we want - * to clear the error as soon as the user enters a valid value in the field - */ - const serverErrKey = getErrorMapKey('server') - if ( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.state.errorMap?.[serverErrKey] && - cause !== 'server' && - !hasErrored - ) { - this.baseStore.setState((prev) => ({ - ...prev, - errorMap: { - ...prev.errorMap, - [serverErrKey]: undefined, - }, - })) + if (formError || fieldErrors) { + hasErrored = true } - }) + } + + /** + * when we have an error for onSubmit in the state, we want + * to clear the error as soon as the user enters a valid value in the field + */ + const submitErrKey = getErrorMapKey('submit') + if ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.state.errorMap?.[submitErrKey] && + cause !== 'submit' && + !hasErrored + ) { + this.baseStore.setState((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + [submitErrKey]: undefined, + }, + })) + } + + /** + * when we have an error for onServer in the state, we want + * to clear the error as soon as the user enters a valid value in the field + */ + const serverErrKey = getErrorMapKey('server') + if ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.state.errorMap?.[serverErrKey] && + cause !== 'server' && + !hasErrored + ) { + this.baseStore.setState((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + [serverErrKey]: undefined, + }, + })) + } + // }) return { hasErrored, fieldsErrorMap: currentValidationErrorMap } } @@ -2061,18 +2080,18 @@ export class FormApi< isSubmitSuccessful: false, // Reset isSubmitSuccessful at the start of submission })) - batch(() => { - void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( - (field) => { - if (!field.instance) return - // If any fields are not touched - if (!field.instance.state.meta.isTouched) { - // Mark them as touched - field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) - } - }, - ) - }) + // batch(() => { + void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( + (field) => { + if (!field.instance) return + // If any fields are not touched + if (!field.instance.state.meta.isTouched) { + // Mark them as touched + field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) + } + }, + ) + // }) const submitMetaArg = submitMeta ?? (this.options.onSubmitMeta as TSubmitMeta) @@ -2138,16 +2157,16 @@ export class FormApi< return } - batch(() => { - void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( - (field) => { - field.instance?.options.listeners?.onSubmit?.({ - value: field.instance.state.value, - fieldApi: field.instance, - }) - }, - ) - }) + // batch(() => { + void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( + (field) => { + field.instance?.options.listeners?.onSubmit?.({ + value: field.instance.state.value, + fieldApi: field.instance, + }) + }, + ) + // }) this.options.listeners?.onSubmit?.({ formApi: this, meta: submitMetaArg }) @@ -2159,21 +2178,21 @@ export class FormApi< meta: submitMetaArg, }) - batch(() => { - this.baseStore.setState((prev) => ({ - ...prev, - isSubmitted: true, - isSubmitSuccessful: true, // Set isSubmitSuccessful to true on successful submission - })) - - formEventClient.emit('form-submission', { - id: this._formId, - submissionAttempt: this.state.submissionAttempts, - successful: true, - }) + // batch(() => { + this.baseStore.setState((prev) => ({ + ...prev, + isSubmitted: true, + isSubmitSuccessful: true, // Set isSubmitSuccessful to true on successful submission + })) - done() + formEventClient.emit('form-submission', { + id: this._formId, + submissionAttempt: this.state.submissionAttempts, + successful: true, }) + + done() + // }) } catch (err) { this.baseStore.setState((prev) => ({ ...prev, @@ -2279,27 +2298,27 @@ export class FormApi< const dontRunListeners = opts?.dontRunListeners ?? false const dontValidate = opts?.dontValidate ?? false - batch(() => { - if (!dontUpdateMeta) { - this.setFieldMeta(field, (prev) => ({ - ...prev, - isTouched: true, - isDirty: true, - errorMap: { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - ...prev?.errorMap, - onMount: undefined, - }, - })) - } + // batch(() => { + if (!dontUpdateMeta) { + this.setFieldMeta(field, (prev) => ({ + ...prev, + isTouched: true, + isDirty: true, + errorMap: { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + ...prev?.errorMap, + onMount: undefined, + }, + })) + } - this.baseStore.setState((prev) => { - return { - ...prev, - values: setBy(prev.values, field, updater), - } - }) + this.baseStore.setState((prev) => { + return { + ...prev, + values: setBy(prev.values, field, updater), + } }) + // }) if (!dontRunListeners) { this.getFieldInfo(field).instance?.triggerOnChangeListener() @@ -2584,50 +2603,50 @@ export class FormApi< UnwrapFormAsyncValidateOrFn >, ) => { - batch(() => { - Object.entries(errorMap).forEach(([key, value]) => { - const errorMapKey = key as ValidationErrorMapKeys + // batch(() => { + Object.entries(errorMap).forEach(([key, value]) => { + const errorMapKey = key as ValidationErrorMapKeys - if (isGlobalFormValidationError(value)) { - const { formError, fieldErrors } = normalizeError(value) + if (isGlobalFormValidationError(value)) { + const { formError, fieldErrors } = normalizeError(value) - for (const fieldName of Object.keys( - this.fieldInfo, - ) as DeepKeys[]) { - const fieldMeta = this.getFieldMeta(fieldName) - if (!fieldMeta) continue - - this.setFieldMeta(fieldName, (prev) => ({ - ...prev, - errorMap: { - ...prev.errorMap, - [errorMapKey]: fieldErrors?.[fieldName], - }, - errorSourceMap: { - ...prev.errorSourceMap, - [errorMapKey]: 'form', - }, - })) - } + for (const fieldName of Object.keys( + this.fieldInfo, + ) as DeepKeys[]) { + const fieldMeta = this.getFieldMeta(fieldName) + if (!fieldMeta) continue - this.baseStore.setState((prev) => ({ + this.setFieldMeta(fieldName, (prev) => ({ ...prev, errorMap: { ...prev.errorMap, - [errorMapKey]: formError, + [errorMapKey]: fieldErrors?.[fieldName], }, - })) - } else { - this.baseStore.setState((prev) => ({ - ...prev, - errorMap: { - ...prev.errorMap, - [errorMapKey]: value, + errorSourceMap: { + ...prev.errorSourceMap, + [errorMapKey]: 'form', }, })) } - }) + + this.baseStore.setState((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + [errorMapKey]: formError, + }, + })) + } else { + this.baseStore.setState((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + [errorMapKey]: value, + }, + })) + } }) + // }) } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92c87132a..431e67c9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1237,8 +1237,8 @@ importers: specifier: ^0.1.1 version: 0.1.1 '@tanstack/store': - specifier: ^0.7.7 - version: 0.7.7 + specifier: https://pkg.pr.new/@tanstack/store@265 + version: https://pkg.pr.new/@tanstack/store@265 devDependencies: arktype: specifier: ^2.1.22 @@ -1486,7 +1486,7 @@ importers: devDependencies: '@sveltejs/package': specifier: ^2.5.3 - version: 2.5.4(svelte@5.41.1)(typescript@5.9.3) + version: 2.5.4(svelte@5.41.1)(typescript@5.8.2) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 version: 5.1.1(svelte@5.41.1)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) @@ -1498,7 +1498,7 @@ importers: version: 5.41.1 svelte-check: specifier: ^4.3.1 - version: 4.3.3(picomatch@4.0.3)(svelte@5.41.1)(typescript@5.9.3) + version: 4.3.3(picomatch@4.0.3)(svelte@5.41.1)(typescript@5.8.2) packages/vue-form: dependencies: @@ -4937,6 +4937,10 @@ packages: '@tanstack/store@0.8.0': resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} + '@tanstack/store@https://pkg.pr.new/@tanstack/store@265': + resolution: {tarball: https://pkg.pr.new/@tanstack/store@265} + version: 0.8.0 + '@tanstack/svelte-store@0.7.7': resolution: {integrity: sha512-JeDyY7SxBi6EKzkf2wWoghdaC2bvmwNL9X/dgkx7LKEvJVle+te7tlELI3cqRNGbjXt9sx+97jx9M5dCCHcuog==} peerDependencies: @@ -9830,10 +9834,12 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} @@ -14109,14 +14115,14 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/package@2.5.4(svelte@5.41.1)(typescript@5.9.3)': + '@sveltejs/package@2.5.4(svelte@5.41.1)(typescript@5.8.2)': dependencies: chokidar: 4.0.3 kleur: 4.1.5 sade: 1.8.1 semver: 7.7.2 svelte: 5.41.1 - svelte2tsx: 0.7.43(svelte@5.41.1)(typescript@5.9.3) + svelte2tsx: 0.7.43(svelte@5.41.1)(typescript@5.8.2) transitivePeerDependencies: - typescript @@ -14450,7 +14456,7 @@ snapshots: '@tanstack/react-router': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) vite: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) - webpack: 5.101.2(@swc/core@1.13.5)(esbuild@0.25.9) + webpack: 5.101.2(@swc/core@1.13.5) transitivePeerDependencies: - supports-color @@ -14557,6 +14563,8 @@ snapshots: '@tanstack/store@0.8.0': {} + '@tanstack/store@https://pkg.pr.new/@tanstack/store@265': {} + '@tanstack/svelte-store@0.7.7(svelte@5.41.1)': dependencies: '@tanstack/store': 0.7.7 @@ -20324,7 +20332,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.41.1)(typescript@5.9.3): + svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.41.1)(typescript@5.8.2): dependencies: '@jridgewell/trace-mapping': 0.3.29 chokidar: 4.0.3 @@ -20332,16 +20340,16 @@ snapshots: picocolors: 1.1.1 sade: 1.8.1 svelte: 5.41.1 - typescript: 5.9.3 + typescript: 5.8.2 transitivePeerDependencies: - picomatch - svelte2tsx@0.7.43(svelte@5.41.1)(typescript@5.9.3): + svelte2tsx@0.7.43(svelte@5.41.1)(typescript@5.8.2): dependencies: dedent-js: 1.0.1 pascal-case: 3.1.2 svelte: 5.41.1 - typescript: 5.9.3 + typescript: 5.8.2 svelte@5.41.1: dependencies: @@ -20413,6 +20421,18 @@ snapshots: '@swc/core': 1.13.5 esbuild: 0.25.9 + terser-webpack-plugin@5.3.14(@swc/core@1.13.5)(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.25.9)): + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + jest-worker: 27.5.1 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + terser: 5.43.1 + webpack: 5.101.2(@swc/core@1.13.5) + optionalDependencies: + '@swc/core': 1.13.5 + optional: true + terser@5.43.1: dependencies: '@jridgewell/source-map': 0.3.6 @@ -20598,7 +20618,8 @@ snapshots: typescript@5.9.2: {} - typescript@5.9.3: {} + typescript@5.9.3: + optional: true uc.micro@2.1.0: {} @@ -21166,6 +21187,39 @@ snapshots: webpack-virtual-modules@0.6.2: {} + webpack@5.101.2(@swc/core@1.13.5): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.25.4 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.1 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.2 + tapable: 2.2.2 + terser-webpack-plugin: 5.3.14(@swc/core@1.13.5)(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.25.9)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + optional: true + webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.25.9): dependencies: '@types/eslint-scope': 3.7.7 From 8128dd4a2845c5e56d2eaecb98e818170c60acd4 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 16:40:33 -0800 Subject: [PATCH 2/5] chore: pass all core tests --- packages/form-core/src/FieldGroupApi.ts | 49 ++++++++++------------ packages/form-core/tests/FieldApi.spec.ts | 3 +- packages/form-core/tests/fieldMeta.spec.ts | 4 +- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/packages/form-core/src/FieldGroupApi.ts b/packages/form-core/src/FieldGroupApi.ts index e69a29fc9..817ba29c0 100644 --- a/packages/form-core/src/FieldGroupApi.ts +++ b/packages/form-core/src/FieldGroupApi.ts @@ -1,4 +1,4 @@ -import { Derived } from '@tanstack/store' +import { createStore, type Store } from '@tanstack/store' import { concatenatePaths, getBy, makePathArray } from './utils' import type { Updater } from './utils' import type { @@ -225,7 +225,7 @@ export class FieldGroupApi< return newProps } - store: Derived> + store: Store> get state() { return this.store.state @@ -275,39 +275,34 @@ export class FieldGroupApi< this.fieldsMap = opts.fields } - this.store = new Derived({ - deps: [this.form.store], - fn: ({ currDepVals }) => { - const currFormStore = currDepVals[0] - let values: TFieldGroupData - if (typeof this.fieldsMap === 'string') { - // all values live at that name, so we can directly fetch it - values = getBy(currFormStore.values, this.fieldsMap) - } else { - // we need to fetch the values from all places where they were mapped from - values = {} as never - const fields: Record = this - .fieldsMap as never - for (const key in fields) { - values[key] = getBy(currFormStore.values, fields[key]) - } + this.store = createStore(() => { + const currFormStore = this.form.store.get() + let values: TFieldGroupData + if (typeof this.fieldsMap === 'string') { + // all values live at that name, so we can directly fetch it + values = getBy(currFormStore.values, this.fieldsMap) + } else { + // we need to fetch the values from all places where they were mapped from + values = {} as never + const fields: Record = this + .fieldsMap as never + for (const key in fields) { + values[key] = getBy(currFormStore.values, fields[key]) } + } - return { - values, - } - }, + return { + values, + } }) } /** * Mounts the field group instance to listen to value changes. + * + * TODO: Remove */ - mount = () => { - const cleanup = this.store.mount() - - return cleanup - } + mount = () => {} /** * Validates the children of a specified array in the form starting from a given index until the end using the correct handlers for a given validation type. diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index d4c9ce3fe..f5dcdaa54 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -1596,8 +1596,7 @@ describe('field api', () => { name: 'name', }) - const unmount = field.mount() - unmount() + field.mount() expect(form.getFieldInfo(field.name).instance).toBeDefined() expect(form.getFieldInfo(field.name)).toBeDefined() }) diff --git a/packages/form-core/tests/fieldMeta.spec.ts b/packages/form-core/tests/fieldMeta.spec.ts index 33b4b96df..e1aa59081 100644 --- a/packages/form-core/tests/fieldMeta.spec.ts +++ b/packages/form-core/tests/fieldMeta.spec.ts @@ -141,14 +141,12 @@ describe('fieldMeta accessing', () => { name: 'name', }) - const cleanup = field.mount() + field.mount() field.setValue('test') expect(form.state.fieldMeta.name?.isTouched).toBe(true) expect(form.state.fieldMeta.name?.isDirty).toBe(true) - cleanup() - const metaAfterCleanup = form.state.fieldMeta.name expect(metaAfterCleanup).toBeDefined() From 5147c241b7558bf8dca055d4487670909ca22211 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 16:45:17 -0800 Subject: [PATCH 3/5] chore: migrate React Form as well --- packages/react-form/package.json | 2 +- packages/react-form/src/useForm.tsx | 1 - packages/react-form/tests/useForm.test.tsx | 2 +- pnpm-lock.yaml | 90 ++++++++-------------- 4 files changed, 34 insertions(+), 61 deletions(-) diff --git a/packages/react-form/package.json b/packages/react-form/package.json index cef854405..fb11b0166 100644 --- a/packages/react-form/package.json +++ b/packages/react-form/package.json @@ -52,7 +52,7 @@ ], "dependencies": { "@tanstack/form-core": "workspace:*", - "@tanstack/react-store": "^0.8.0" + "@tanstack/react-store": "https://pkg.pr.new/@tanstack/react-store@265" }, "devDependencies": { "@types/react": "^19.0.7", diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 525559675..2c4fb572c 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -16,7 +16,6 @@ import type { } from '@tanstack/form-core' import type { FunctionComponent, PropsWithChildren, ReactNode } from 'react' import type { FieldComponent } from './useField' -import type { NoInfer } from '@tanstack/react-store' /** * Fields that are added onto the `FormAPI` from `@tanstack/form-core` and returned from `useForm` diff --git a/packages/react-form/tests/useForm.test.tsx b/packages/react-form/tests/useForm.test.tsx index dbb7ff845..74cc11f22 100644 --- a/packages/react-form/tests/useForm.test.tsx +++ b/packages/react-form/tests/useForm.test.tsx @@ -781,7 +781,7 @@ describe('useForm', () => { }, }) - const { values } = useStore(form.store) + const { values } = useStore(form.store, (v) => v) useEffect(() => { fn(values) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 431e67c9e..8d54768a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1297,8 +1297,8 @@ importers: specifier: workspace:* version: link:../form-core '@tanstack/react-store': - specifier: ^0.8.0 - version: 0.8.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: https://pkg.pr.new/@tanstack/react-store@265 + version: https://pkg.pr.new/@tanstack/react-store@265(react-dom@19.1.0(react@19.1.0))(react@19.1.0) devDependencies: '@types/react': specifier: ^19.0.7 @@ -1486,7 +1486,7 @@ importers: devDependencies: '@sveltejs/package': specifier: ^2.5.3 - version: 2.5.4(svelte@5.41.1)(typescript@5.8.2) + version: 2.5.4(svelte@5.41.1)(typescript@5.9.3) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 version: 5.1.1(svelte@5.41.1)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) @@ -1498,7 +1498,7 @@ importers: version: 5.41.1 svelte-check: specifier: ^4.3.1 - version: 4.3.3(picomatch@4.0.3)(svelte@5.41.1)(typescript@5.8.2) + version: 4.3.3(picomatch@4.0.3)(svelte@5.41.1)(typescript@5.9.3) packages/vue-form: dependencies: @@ -4865,6 +4865,13 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/react-store@https://pkg.pr.new/@tanstack/react-store@265': + resolution: {tarball: https://pkg.pr.new/@tanstack/react-store@265} + version: 0.8.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/router-core@1.135.2': resolution: {integrity: sha512-fhJSGmbqE78Ou6e+cnJ9exmjCzCZ9IXT2rApiPAgeItKj2yy1qmTEoR11n0x0fiNkkBxHL1us+QyG8JfNELiQA==} engines: {node: '>=12'} @@ -4941,6 +4948,10 @@ packages: resolution: {tarball: https://pkg.pr.new/@tanstack/store@265} version: 0.8.0 + '@tanstack/store@https://pkg.pr.new/TanStack/store/@tanstack/store@6bcd72e': + resolution: {tarball: https://pkg.pr.new/TanStack/store/@tanstack/store@6bcd72e} + version: 0.8.0 + '@tanstack/svelte-store@0.7.7': resolution: {integrity: sha512-JeDyY7SxBi6EKzkf2wWoghdaC2bvmwNL9X/dgkx7LKEvJVle+te7tlELI3cqRNGbjXt9sx+97jx9M5dCCHcuog==} peerDependencies: @@ -14115,14 +14126,14 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/package@2.5.4(svelte@5.41.1)(typescript@5.8.2)': + '@sveltejs/package@2.5.4(svelte@5.41.1)(typescript@5.9.3)': dependencies: chokidar: 4.0.3 kleur: 4.1.5 sade: 1.8.1 semver: 7.7.2 svelte: 5.41.1 - svelte2tsx: 0.7.43(svelte@5.41.1)(typescript@5.8.2) + svelte2tsx: 0.7.43(svelte@5.41.1)(typescript@5.9.3) transitivePeerDependencies: - typescript @@ -14413,6 +14424,13 @@ snapshots: react-dom: 19.1.0(react@19.1.0) use-sync-external-store: 1.6.0(react@19.1.0) + '@tanstack/react-store@https://pkg.pr.new/@tanstack/react-store@265(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/store': https://pkg.pr.new/TanStack/store/@tanstack/store@6bcd72e + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + use-sync-external-store: 1.6.0(react@19.1.0) + '@tanstack/router-core@1.135.2': dependencies: '@tanstack/history': 1.133.28 @@ -14456,7 +14474,7 @@ snapshots: '@tanstack/react-router': 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) vite: 7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1) vite-plugin-solid: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.2.2(@types/node@24.1.0)(jiti@2.6.1)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1)(tsx@4.19.4)(yaml@2.8.1)) - webpack: 5.101.2(@swc/core@1.13.5) + webpack: 5.101.2(@swc/core@1.13.5)(esbuild@0.25.9) transitivePeerDependencies: - supports-color @@ -14565,6 +14583,8 @@ snapshots: '@tanstack/store@https://pkg.pr.new/@tanstack/store@265': {} + '@tanstack/store@https://pkg.pr.new/TanStack/store/@tanstack/store@6bcd72e': {} + '@tanstack/svelte-store@0.7.7(svelte@5.41.1)': dependencies: '@tanstack/store': 0.7.7 @@ -20332,7 +20352,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.41.1)(typescript@5.8.2): + svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.41.1)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.29 chokidar: 4.0.3 @@ -20340,16 +20360,16 @@ snapshots: picocolors: 1.1.1 sade: 1.8.1 svelte: 5.41.1 - typescript: 5.8.2 + typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte2tsx@0.7.43(svelte@5.41.1)(typescript@5.8.2): + svelte2tsx@0.7.43(svelte@5.41.1)(typescript@5.9.3): dependencies: dedent-js: 1.0.1 pascal-case: 3.1.2 svelte: 5.41.1 - typescript: 5.8.2 + typescript: 5.9.3 svelte@5.41.1: dependencies: @@ -20421,18 +20441,6 @@ snapshots: '@swc/core': 1.13.5 esbuild: 0.25.9 - terser-webpack-plugin@5.3.14(@swc/core@1.13.5)(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.25.9)): - dependencies: - '@jridgewell/trace-mapping': 0.3.29 - jest-worker: 27.5.1 - schema-utils: 4.3.2 - serialize-javascript: 6.0.2 - terser: 5.43.1 - webpack: 5.101.2(@swc/core@1.13.5) - optionalDependencies: - '@swc/core': 1.13.5 - optional: true - terser@5.43.1: dependencies: '@jridgewell/source-map': 0.3.6 @@ -20618,8 +20626,7 @@ snapshots: typescript@5.9.2: {} - typescript@5.9.3: - optional: true + typescript@5.9.3: {} uc.micro@2.1.0: {} @@ -21187,39 +21194,6 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.101.2(@swc/core@1.13.5): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.25.4 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.2 - tapable: 2.2.2 - terser-webpack-plugin: 5.3.14(@swc/core@1.13.5)(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.25.9)) - watchpack: 2.4.4 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - optional: true - webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.25.9): dependencies: '@types/eslint-scope': 3.7.7 From 2deddb561a114693d3b85b0591b4682494302698 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:05:00 +0000 Subject: [PATCH 4/5] ci: apply automated fixes and generate docs --- packages/form-core/src/transform.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/form-core/src/transform.ts b/packages/form-core/src/transform.ts index f5dcf420d..382d44be9 100644 --- a/packages/form-core/src/transform.ts +++ b/packages/form-core/src/transform.ts @@ -141,14 +141,14 @@ export function mergeAndUpdate< }, {} as Partial) // batch(() => { - if (Object.keys(diffedObject).length) { - form.baseStore.setState((prev) => ({ ...prev, ...diffedObject })) - } + if (Object.keys(diffedObject).length) { + form.baseStore.setState((prev) => ({ ...prev, ...diffedObject })) + } - if (newObj.state.errorMap !== form.state.errorMap) { - // Check if we need to update `fieldMetaBase` with `errorMaps` set by - form.setErrorMap(newObj.state.errorMap) - } + if (newObj.state.errorMap !== form.state.errorMap) { + // Check if we need to update `fieldMetaBase` with `errorMaps` set by + form.setErrorMap(newObj.state.errorMap) + } // }) return newObj From b134a180df1b222e9444db555b410de103b6da22 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 17 Feb 2026 03:50:18 -0800 Subject: [PATCH 5/5] chore: fix various other issues --- packages/form-core/src/FieldApi.ts | 106 ++-- packages/form-core/src/FieldGroupApi.ts | 6 +- packages/form-core/src/FormApi.ts | 462 +++++++++--------- packages/form-core/src/transform.ts | 20 +- packages/form-core/tests/FieldApi.spec.ts | 8 +- .../lit-form/src/tanstack-form-controller.ts | 2 +- packages/solid-form/tests/createForm.test.tsx | 18 +- packages/vue-form/src/useForm.tsx | 1 - 8 files changed, 318 insertions(+), 305 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 4f709d88b..f8b1b009e 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1,4 +1,4 @@ -import { createStore } from '@tanstack/store' +import { batch, createStore } from '@tanstack/store' import { isStandardSchemaValidator, standardSchemaValidators, @@ -8,7 +8,6 @@ import { determineFieldLevelErrorSourceAndValue, evaluate, getAsyncValidatorArray, - getBy, getSyncValidatorArray, mergeOpts, } from './utils' @@ -1322,6 +1321,9 @@ export class FieldApi< value: this.state.value, fieldApi: this, }) + + // TODO: Remove + return () => {} } /** @@ -1622,62 +1624,62 @@ export class FieldApi< // Needs type cast as eslint errantly believes this is always falsy let hasErrored = false as boolean - // batch(() => { - const validateFieldFn = ( - field: AnyFieldApi, - validateObj: SyncValidator, - ) => { - const errorMapKey = getErrorMapKey(validateObj.cause) + batch(() => { + const validateFieldFn = ( + field: AnyFieldApi, + validateObj: SyncValidator, + ) => { + const errorMapKey = getErrorMapKey(validateObj.cause) + + const fieldLevelError = validateObj.validate + ? normalizeError( + field.runValidator({ + validate: validateObj.validate, + value: { + value: field.store.state.value, + validationSource: 'field', + fieldApi: field, + }, + type: 'validate', + }), + ) + : undefined - const fieldLevelError = validateObj.validate - ? normalizeError( - field.runValidator({ - validate: validateObj.validate, - value: { - value: field.store.state.value, - validationSource: 'field', - fieldApi: field, - }, - type: 'validate', - }), - ) - : undefined + const formLevelError = errorFromForm[errorMapKey] - const formLevelError = errorFromForm[errorMapKey] + const { newErrorValue, newSource } = + determineFieldLevelErrorSourceAndValue({ + formLevelError, + fieldLevelError, + }) - const { newErrorValue, newSource } = - determineFieldLevelErrorSourceAndValue({ - formLevelError, - fieldLevelError, - }) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (field.state.meta.errorMap?.[errorMapKey] !== newErrorValue) { + field.setMeta((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + [errorMapKey]: newErrorValue, + }, + errorSourceMap: { + ...prev.errorSourceMap, + [errorMapKey]: newSource, + }, + })) + } + if (newErrorValue) { + hasErrored = true + } + } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (field.state.meta.errorMap?.[errorMapKey] !== newErrorValue) { - field.setMeta((prev) => ({ - ...prev, - errorMap: { - ...prev.errorMap, - [errorMapKey]: newErrorValue, - }, - errorSourceMap: { - ...prev.errorSourceMap, - [errorMapKey]: newSource, - }, - })) + for (const validateObj of validates) { + validateFieldFn(this, validateObj) } - if (newErrorValue) { - hasErrored = true + for (const fieldValitateObj of linkedFieldValidates) { + if (!fieldValitateObj.validate) continue + validateFieldFn(fieldValitateObj.field, fieldValitateObj) } - } - - for (const validateObj of validates) { - validateFieldFn(this, validateObj) - } - for (const fieldValitateObj of linkedFieldValidates) { - if (!fieldValitateObj.validate) continue - validateFieldFn(fieldValitateObj.field, fieldValitateObj) - } - // }) + }) /** * when we have an error for onSubmit in the state, we want diff --git a/packages/form-core/src/FieldGroupApi.ts b/packages/form-core/src/FieldGroupApi.ts index a22273a91..6cd9d0976 100644 --- a/packages/form-core/src/FieldGroupApi.ts +++ b/packages/form-core/src/FieldGroupApi.ts @@ -1,6 +1,6 @@ import { createStore } from '@tanstack/store' import { concatenatePaths, getBy, makePathArray } from './utils' -import type { ReadonlyStore } from '@tanstack/store'; +import type { ReadonlyStore } from '@tanstack/store' import type { Updater } from './utils' import type { FormApi, @@ -303,7 +303,9 @@ export class FieldGroupApi< * * TODO: Remove */ - mount = () => {} + mount = () => { + return () => {} + } /** * Validates the children of a specified array in the form starting from a given index until the end using the correct handlers for a given validation type. diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 3b5348e7f..d78c39a8a 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1,4 +1,4 @@ -import { createStore } from '@tanstack/store' +import { batch, createStore } from '@tanstack/store' import { deleteBy, determineFormLevelErrorSourceAndValue, @@ -21,7 +21,7 @@ import { } from './standardSchemaValidator' import { defaultFieldMeta, metaHelper } from './metaHelper' import { formEventClient } from './EventClient' -import type {ReadonlyStore, Store} from '@tanstack/store'; +import type { ReadonlyStore, Store } from '@tanstack/store' // types import type { ValidationLogicFn } from './ValidationLogic' @@ -1469,24 +1469,24 @@ export class FormApi< if (!shouldUpdateValues && !shouldUpdateState) return - // batch(() => { - this.baseStore.setState(() => - getDefaultFormState( - Object.assign( - {}, - this.state as any, - - shouldUpdateState ? options.defaultState : {}, - - shouldUpdateValues - ? { - values: options.defaultValues, - } - : {}, + batch(() => { + this.baseStore.setState(() => + getDefaultFormState( + Object.assign( + {}, + this.state as any, + + shouldUpdateState ? options.defaultState : {}, + + shouldUpdateValues + ? { + values: options.defaultValues, + } + : {}, + ), ), - ), - ) - // }) + ) + }) formEventClient.emit('form-api', { id: this._formId, @@ -1530,26 +1530,26 @@ export class FormApi< */ validateAllFields = async (cause: ValidationCause) => { const fieldValidationPromises: Promise[] = [] as any - // batch(() => { - void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( - (field) => { - if (!field.instance) return - const fieldInstance = field.instance - // Validate the field - fieldValidationPromises.push( - // Remember, `validate` is either a sync operation or a promise - Promise.resolve().then(() => - fieldInstance.validate(cause, { skipFormValidation: true }), - ), - ) - // If any fields are not touched - if (!field.instance.state.meta.isTouched) { - // Mark them as touched - field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) - } - }, - ) - // }) + batch(() => { + void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( + (field) => { + if (!field.instance) return + const fieldInstance = field.instance + // Validate the field + fieldValidationPromises.push( + // Remember, `validate` is either a sync operation or a promise + Promise.resolve().then(() => + fieldInstance.validate(cause, { skipFormValidation: true }), + ), + ) + // If any fields are not touched + if (!field.instance.state.meta.isTouched) { + // Mark them as touched + field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) + } + }, + ) + }) const fieldErrorMapMap = await Promise.all(fieldValidationPromises) return fieldErrorMapMap.flat() @@ -1584,13 +1584,13 @@ export class FormApi< // Validate the fields const fieldValidationPromises: Promise[] = [] as any - // batch(() => { - fieldsToValidate.forEach((nestedField) => { - fieldValidationPromises.push( - Promise.resolve().then(() => this.validateField(nestedField, cause)), - ) + batch(() => { + fieldsToValidate.forEach((nestedField) => { + fieldValidationPromises.push( + Promise.resolve().then(() => this.validateField(nestedField, cause)), + ) + }) }) - // }) const fieldErrorMapMap = await Promise.all(fieldValidationPromises) return fieldErrorMapMap.flat() @@ -1670,136 +1670,136 @@ export class FormApi< TOnDynamicAsync > = {} - // batch(() => { - for (const validateObj of validates) { - if (!validateObj.validate) continue + batch(() => { + for (const validateObj of validates) { + if (!validateObj.validate) continue - const rawError = this.runValidator({ - validate: validateObj.validate, - value: { - value: this.state.values, - formApi: this, - validationSource: 'form', - }, - type: 'validate', - }) + const rawError = this.runValidator({ + validate: validateObj.validate, + value: { + value: this.state.values, + formApi: this, + validationSource: 'form', + }, + type: 'validate', + }) - const { formError, fieldErrors } = normalizeError(rawError) + const { formError, fieldErrors } = normalizeError(rawError) - const errorMapKey = getErrorMapKey(validateObj.cause) + const errorMapKey = getErrorMapKey(validateObj.cause) - const allFieldsToProcess = new Set([ - ...Object.keys(this.state.fieldMeta), - ...Object.keys(fieldErrors || {}), - ] as DeepKeys[]) + const allFieldsToProcess = new Set([ + ...Object.keys(this.state.fieldMeta), + ...Object.keys(fieldErrors || {}), + ] as DeepKeys[]) - for (const field of allFieldsToProcess) { - if ( - this.baseStore.state.fieldMetaBase[field] === undefined && - !fieldErrors?.[field] - ) { - continue - } + for (const field of allFieldsToProcess) { + if ( + this.baseStore.state.fieldMetaBase[field] === undefined && + !fieldErrors?.[field] + ) { + continue + } - const fieldMeta = this.getFieldMeta(field) ?? defaultFieldMeta - const { - errorMap: currentErrorMap, - errorSourceMap: currentErrorMapSource, - } = fieldMeta + const fieldMeta = this.getFieldMeta(field) ?? defaultFieldMeta + const { + errorMap: currentErrorMap, + errorSourceMap: currentErrorMapSource, + } = fieldMeta - const newFormValidatorError = fieldErrors?.[field] + const newFormValidatorError = fieldErrors?.[field] - const { newErrorValue, newSource } = - determineFormLevelErrorSourceAndValue({ - newFormValidatorError, - isPreviousErrorFromFormValidator: - // These conditional checks are required, otherwise we get runtime errors. + const { newErrorValue, newSource } = + determineFormLevelErrorSourceAndValue({ + newFormValidatorError, + isPreviousErrorFromFormValidator: + // These conditional checks are required, otherwise we get runtime errors. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + currentErrorMapSource?.[errorMapKey] === 'form', // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - currentErrorMapSource?.[errorMapKey] === 'form', - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - previousErrorValue: currentErrorMap?.[errorMapKey], - }) + previousErrorValue: currentErrorMap?.[errorMapKey], + }) + + if (newSource === 'form') { + currentValidationErrorMap[field] = { + ...currentValidationErrorMap[field], + [errorMapKey]: newFormValidatorError, + } + } - if (newSource === 'form') { - currentValidationErrorMap[field] = { - ...currentValidationErrorMap[field], - [errorMapKey]: newFormValidatorError, + // This conditional check is required, otherwise we get runtime errors. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (currentErrorMap?.[errorMapKey] !== newErrorValue) { + this.setFieldMeta(field, (prev = defaultFieldMeta) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + [errorMapKey]: newErrorValue, + }, + errorSourceMap: { + ...prev.errorSourceMap, + [errorMapKey]: newSource, + }, + })) } } - // This conditional check is required, otherwise we get runtime errors. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (currentErrorMap?.[errorMapKey] !== newErrorValue) { - this.setFieldMeta(field, (prev = defaultFieldMeta) => ({ + if (this.state.errorMap?.[errorMapKey] !== formError) { + this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, - [errorMapKey]: newErrorValue, - }, - errorSourceMap: { - ...prev.errorSourceMap, - [errorMapKey]: newSource, + [errorMapKey]: formError, }, })) } + + if (formError || fieldErrors) { + hasErrored = true + } } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.state.errorMap?.[errorMapKey] !== formError) { + /** + * when we have an error for onSubmit in the state, we want + * to clear the error as soon as the user enters a valid value in the field + */ + const submitErrKey = getErrorMapKey('submit') + if ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.state.errorMap?.[submitErrKey] && + cause !== 'submit' && + !hasErrored + ) { this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, - [errorMapKey]: formError, + [submitErrKey]: undefined, }, })) } - if (formError || fieldErrors) { - hasErrored = true + /** + * when we have an error for onServer in the state, we want + * to clear the error as soon as the user enters a valid value in the field + */ + const serverErrKey = getErrorMapKey('server') + if ( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.state.errorMap?.[serverErrKey] && + cause !== 'server' && + !hasErrored + ) { + this.baseStore.setState((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + [serverErrKey]: undefined, + }, + })) } - } - - /** - * when we have an error for onSubmit in the state, we want - * to clear the error as soon as the user enters a valid value in the field - */ - const submitErrKey = getErrorMapKey('submit') - if ( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.state.errorMap?.[submitErrKey] && - cause !== 'submit' && - !hasErrored - ) { - this.baseStore.setState((prev) => ({ - ...prev, - errorMap: { - ...prev.errorMap, - [submitErrKey]: undefined, - }, - })) - } - - /** - * when we have an error for onServer in the state, we want - * to clear the error as soon as the user enters a valid value in the field - */ - const serverErrKey = getErrorMapKey('server') - if ( - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.state.errorMap?.[serverErrKey] && - cause !== 'server' && - !hasErrored - ) { - this.baseStore.setState((prev) => ({ - ...prev, - errorMap: { - ...prev.errorMap, - [serverErrKey]: undefined, - }, - })) - } - // }) + }) return { hasErrored, fieldsErrorMap: currentValidationErrorMap } } @@ -2067,18 +2067,18 @@ export class FormApi< isSubmitSuccessful: false, // Reset isSubmitSuccessful at the start of submission })) - // batch(() => { - void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( - (field) => { - if (!field.instance) return - // If any fields are not touched - if (!field.instance.state.meta.isTouched) { - // Mark them as touched - field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) - } - }, - ) - // }) + batch(() => { + void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( + (field) => { + if (!field.instance) return + // If any fields are not touched + if (!field.instance.state.meta.isTouched) { + // Mark them as touched + field.instance.setMeta((prev) => ({ ...prev, isTouched: true })) + } + }, + ) + }) const submitMetaArg = submitMeta ?? (this.options.onSubmitMeta as TSubmitMeta) @@ -2144,16 +2144,16 @@ export class FormApi< return } - // batch(() => { - void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( - (field) => { - field.instance?.options.listeners?.onSubmit?.({ - value: field.instance.state.value, - fieldApi: field.instance, - }) - }, - ) - // }) + batch(() => { + void (Object.values(this.fieldInfo) as FieldInfo[]).forEach( + (field) => { + field.instance?.options.listeners?.onSubmit?.({ + value: field.instance.state.value, + fieldApi: field.instance, + }) + }, + ) + }) this.options.listeners?.onSubmit?.({ formApi: this, meta: submitMetaArg }) @@ -2165,21 +2165,21 @@ export class FormApi< meta: submitMetaArg, }) - // batch(() => { - this.baseStore.setState((prev) => ({ - ...prev, - isSubmitted: true, - isSubmitSuccessful: true, // Set isSubmitSuccessful to true on successful submission - })) + batch(() => { + this.baseStore.setState((prev) => ({ + ...prev, + isSubmitted: true, + isSubmitSuccessful: true, // Set isSubmitSuccessful to true on successful submission + })) - formEventClient.emit('form-submission', { - id: this._formId, - submissionAttempt: this.state.submissionAttempts, - successful: true, - }) + formEventClient.emit('form-submission', { + id: this._formId, + submissionAttempt: this.state.submissionAttempts, + successful: true, + }) - done() - // }) + done() + }) } catch (err) { this.baseStore.setState((prev) => ({ ...prev, @@ -2285,27 +2285,27 @@ export class FormApi< const dontRunListeners = opts?.dontRunListeners ?? false const dontValidate = opts?.dontValidate ?? false - // batch(() => { - if (!dontUpdateMeta) { - this.setFieldMeta(field, (prev) => ({ - ...prev, - isTouched: true, - isDirty: true, - errorMap: { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - ...prev?.errorMap, - onMount: undefined, - }, - })) - } - - this.baseStore.setState((prev) => { - return { - ...prev, - values: setBy(prev.values, field, updater), + batch(() => { + if (!dontUpdateMeta) { + this.setFieldMeta(field, (prev) => ({ + ...prev, + isTouched: true, + isDirty: true, + errorMap: { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + ...prev?.errorMap, + onMount: undefined, + }, + })) } + + this.baseStore.setState((prev) => { + return { + ...prev, + values: setBy(prev.values, field, updater), + } + }) }) - // }) if (!dontRunListeners) { this.getFieldInfo(field).instance?.triggerOnChangeListener() @@ -2590,50 +2590,50 @@ export class FormApi< UnwrapFormAsyncValidateOrFn >, ) => { - // batch(() => { - Object.entries(errorMap).forEach(([key, value]) => { - const errorMapKey = key as ValidationErrorMapKeys + batch(() => { + Object.entries(errorMap).forEach(([key, value]) => { + const errorMapKey = key as ValidationErrorMapKeys - if (isGlobalFormValidationError(value)) { - const { formError, fieldErrors } = normalizeError(value) + if (isGlobalFormValidationError(value)) { + const { formError, fieldErrors } = normalizeError(value) - for (const fieldName of Object.keys( - this.fieldInfo, - ) as DeepKeys[]) { - const fieldMeta = this.getFieldMeta(fieldName) - if (!fieldMeta) continue + for (const fieldName of Object.keys( + this.fieldInfo, + ) as DeepKeys[]) { + const fieldMeta = this.getFieldMeta(fieldName) + if (!fieldMeta) continue + + this.setFieldMeta(fieldName, (prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + [errorMapKey]: fieldErrors?.[fieldName], + }, + errorSourceMap: { + ...prev.errorSourceMap, + [errorMapKey]: 'form', + }, + })) + } - this.setFieldMeta(fieldName, (prev) => ({ + this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, - [errorMapKey]: fieldErrors?.[fieldName], + [errorMapKey]: formError, }, - errorSourceMap: { - ...prev.errorSourceMap, - [errorMapKey]: 'form', + })) + } else { + this.baseStore.setState((prev) => ({ + ...prev, + errorMap: { + ...prev.errorMap, + [errorMapKey]: value, }, })) } - - this.baseStore.setState((prev) => ({ - ...prev, - errorMap: { - ...prev.errorMap, - [errorMapKey]: formError, - }, - })) - } else { - this.baseStore.setState((prev) => ({ - ...prev, - errorMap: { - ...prev.errorMap, - [errorMapKey]: value, - }, - })) - } + }) }) - // }) } /** diff --git a/packages/form-core/src/transform.ts b/packages/form-core/src/transform.ts index 382d44be9..d5f0276d6 100644 --- a/packages/form-core/src/transform.ts +++ b/packages/form-core/src/transform.ts @@ -1,4 +1,4 @@ -// import { batch } from '@tanstack/store' +import { batch } from '@tanstack/store' import { deepCopy } from './utils' import type { AnyBaseFormState, @@ -140,16 +140,16 @@ export function mergeAndUpdate< return prev }, {} as Partial) - // batch(() => { - if (Object.keys(diffedObject).length) { - form.baseStore.setState((prev) => ({ ...prev, ...diffedObject })) - } + batch(() => { + if (Object.keys(diffedObject).length) { + form.baseStore.setState((prev) => ({ ...prev, ...diffedObject })) + } - if (newObj.state.errorMap !== form.state.errorMap) { - // Check if we need to update `fieldMetaBase` with `errorMaps` set by - form.setErrorMap(newObj.state.errorMap) - } - // }) + if (newObj.state.errorMap !== form.state.errorMap) { + // Check if we need to update `fieldMetaBase` with `errorMaps` set by + form.setErrorMap(newObj.state.errorMap) + } + }) return newObj } diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index f5dcdaa54..f9d099022 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -817,13 +817,13 @@ describe('field api', () => { // No async validators defined - only sync or none }) - field.mount() + const unsub = field.mount() // Track isValidating changes const isValidatingStates: boolean[] = [] - field.store.subscribe(() => { + const storeunsub = field.store.subscribe(() => { isValidatingStates.push(field.getMeta().isValidating) - }) + }).unsubscribe // Initial state expect(field.getMeta().isValidating).toBe(false) @@ -836,6 +836,8 @@ describe('field api', () => { // This prevents unnecessary re-renders expect(isValidatingStates.every((state) => state === false)).toBe(true) expect(field.getMeta().isValidating).toBe(false) + unsub() + storeunsub() }) it('should run async validation onChange', async () => { diff --git a/packages/lit-form/src/tanstack-form-controller.ts b/packages/lit-form/src/tanstack-form-controller.ts index 64bbeb9e2..1ebf38c97 100644 --- a/packages/lit-form/src/tanstack-form-controller.ts +++ b/packages/lit-form/src/tanstack-form-controller.ts @@ -288,7 +288,7 @@ export class TanStackFormController< hostConnected() { this.#subscription = this.api.store.subscribe(() => { this.#host.requestUpdate() - }) + }).unsubscribe } hostDisconnected() { diff --git a/packages/solid-form/tests/createForm.test.tsx b/packages/solid-form/tests/createForm.test.tsx index 1064a4d04..d4ec4526f 100644 --- a/packages/solid-form/tests/createForm.test.tsx +++ b/packages/solid-form/tests/createForm.test.tsx @@ -214,7 +214,9 @@ describe('createForm', () => { })) const [errors, setErrors] = createSignal() - onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) + onCleanup( + form.store.subscribe(() => setErrors(form.state.errorMap)).unsubscribe, + ) return ( <> @@ -271,7 +273,9 @@ describe('createForm', () => { })) const [errors, setErrors] = createSignal() - onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) + onCleanup( + form.store.subscribe(() => setErrors(form.state.errorMap)).unsubscribe, + ) return ( <> @@ -325,7 +329,9 @@ describe('createForm', () => { })) const [errors, setErrors] = createSignal() - onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) + onCleanup( + form.store.subscribe(() => setErrors(form.state.errorMap)).unsubscribe, + ) return ( <> @@ -381,7 +387,9 @@ describe('createForm', () => { })) const [errors, setErrors] = createSignal() - onCleanup(form.store.subscribe(() => setErrors(form.state.errorMap))) + onCleanup( + form.store.subscribe(() => setErrors(form.state.errorMap)).unsubscribe, + ) return ( <> @@ -444,7 +452,7 @@ describe('createForm', () => { onCleanup( form.store.subscribe(() => setErrors(form.state.errorMap.onChange?.toString() || ''), - ), + ).unsubscribe, ) return ( diff --git a/packages/vue-form/src/useForm.tsx b/packages/vue-form/src/useForm.tsx index e83e50015..74be6e56a 100644 --- a/packages/vue-form/src/useForm.tsx +++ b/packages/vue-form/src/useForm.tsx @@ -8,7 +8,6 @@ import type { FormState, FormValidateOrFn, } from '@tanstack/form-core' -import type { NoInfer } from '@tanstack/vue-store' import type { ComponentOptionsMixin, CreateComponentPublicInstanceWithMixins,