From 9141330006cdecf604c40940bb440b966829fec3 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 16:39:14 -0800 Subject: [PATCH 01/31] 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 02/31] 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 03/31] 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 98db8a7a3a480a329f14e7cdd8f9168ed6e3e66c Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 16:55:48 -0800 Subject: [PATCH 04/31] chore: add initial benchmarks --- packages/react-form/package.json | 9 +- .../tests/onchange-detection.bench.tsx | 273 ++++ packages/react-form/tsconfig.json | 8 +- pnpm-lock.yaml | 1097 ++++++++++++++++- 4 files changed, 1357 insertions(+), 30 deletions(-) create mode 100644 packages/react-form/tests/onchange-detection.bench.tsx diff --git a/packages/react-form/package.json b/packages/react-form/package.json index 731c1cb2f..1c60ae2b6 100644 --- a/packages/react-form/package.json +++ b/packages/react-form/package.json @@ -24,6 +24,7 @@ "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js", "test:types:ts58": "tsc", "test:lib": "vitest", + "bench": "vitest bench", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict", "build": "vite build" @@ -55,13 +56,19 @@ "@tanstack/react-store": "https://pkg.pr.new/@tanstack/react-store@265" }, "devDependencies": { + "@hookform/error-message": "^2.0.1", + "@hookform/resolvers": "^5.2.2", "@types/react": "^19.0.7", "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^5.1.1", "eslint-plugin-react-compiler": "19.1.0-rc.2", + "formik": "^2.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", - "vite": "^7.2.2" + "react-hook-form": "^7.54.2", + "vite": "^7.2.2", + "zod": "^4.3.6", + "zod-formik-adapter": "^2.0.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/packages/react-form/tests/onchange-detection.bench.tsx b/packages/react-form/tests/onchange-detection.bench.tsx new file mode 100644 index 000000000..224438ac6 --- /dev/null +++ b/packages/react-form/tests/onchange-detection.bench.tsx @@ -0,0 +1,273 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +import { bench, describe } from 'vitest' + +import { z } from 'zod' +import { cleanup, fireEvent, render } from '@testing-library/react' +import { Formik, Field as FormikField } from 'formik' +import { toFormikValidationSchema } from 'zod-formik-adapter' +import { Controller, useForm as useReactHookForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { ErrorMessage } from '@hookform/error-message' +import { useForm as useTanStackForm } from '../src' +import type { FieldProps } from 'formik/dist/Field' + +const arr = Array.from({ length: 100 }, (_, i) => i) + +const validators = { + onChange: z.number().min(3, 'Must be at least three'), +} + +function TanStackFormOnChangeBenchmark() { + const form = useTanStackForm({ + defaultValues: { num: arr }, + }) + + return ( + <> + {arr.map((_num, i) => { + return ( + + {(field) => { + return ( +
+ field.handleChange(e.target.valueAsNumber)} + placeholder={`Number ${i}`} + /> + {field.state.meta.errors.map((error) => ( +

{error?.message}

+ ))} +
+ ) + }} +
+ ) + })} + + ) +} + +function FormikOnChangeBenchmark() { + return ( + {}} + > + {() => ( + <> + {arr.map((_num, i) => ( + + {(props: FieldProps) => ( +
+ + {props.meta.error} +
+ )} +
+ ))} + + )} +
+ ) +} + +function ReactHookFormOnChangeBenchmark() { + const { + register, + formState: { errors }, + } = useReactHookForm({ + defaultValues: { + num: arr, + }, + mode: 'onChange', + resolver: zodResolver( + z.object({ + num: z.array(z.number().min(3, 'Must be at least three')), + }), + ), + }) + + return ( + <> + {arr.map((_num, i) => { + return ( +
+ + +
+ ) + })} + + ) +} + +function ReactHookFormHeadlessOnChangeBenchmark() { + const { control, handleSubmit } = useReactHookForm({ + defaultValues: { + num: arr, + }, + mode: 'onChange', + resolver: zodResolver( + z.object({ + num: z.array(z.number().min(3, 'Must be at least three')), + }), + ), + }) + + return ( + <> + {arr.map((_num, i) => { + return ( + { + return ( +
+ onChange(event.target.valueAsNumber)} + placeholder={`Number ${i}`} + /> + {error &&

{error.message}

} +
+ ) + }} + name={`num.${i}`} + /> + ) + })} + + ) +} + +describe('Validates onChange on 1,000 form items', () => { + bench( + 'TanStack Form', + async () => { + const { getByTestId, findAllByText, queryAllByText } = render( + , + ) + + if (queryAllByText('Must be at least three')?.length) { + throw 'Should not be present yet' + } + + fireEvent.change(getByTestId('value1'), { target: { value: 0 } }) + + await findAllByText('Must be at least three') + }, + { + setup(task) { + task.opts.beforeEach = () => { + cleanup() + } + }, + }, + ) + + bench( + 'Formik', + async () => { + const { getByTestId, findAllByText, queryAllByText } = render( + , + ) + + if (queryAllByText('Must be at least three')?.length) { + throw 'Should not be present yet' + } + + fireEvent.change(getByTestId('value1'), { target: { value: 0 } }) + + await findAllByText('Must be at least three') + }, + { + setup(task) { + task.opts.beforeEach = () => { + cleanup() + } + }, + }, + ) + + bench( + 'React Hook Form', + async () => { + const { getByTestId, findAllByText, queryAllByText } = render( + , + ) + + if (queryAllByText('Must be at least three')?.length) { + throw 'Should not be present yet' + } + + fireEvent.change(getByTestId('value1'), { target: { value: 0 } }) + + await findAllByText('Must be at least three') + }, + { + setup(task) { + task.opts.beforeEach = () => { + cleanup() + } + }, + }, + ) + + bench( + 'React Hook Form (Headless)', + async () => { + const { getByTestId, findAllByText, queryAllByText } = render( + , + ) + + if (queryAllByText('Must be at least three')?.length) { + throw 'Should not be present yet' + } + + fireEvent.change(getByTestId('value1'), { target: { value: 0 } }) + + await findAllByText('Must be at least three') + }, + { + setup(task) { + task.opts.beforeEach = () => { + cleanup() + } + }, + }, + ) +}) diff --git a/packages/react-form/tsconfig.json b/packages/react-form/tsconfig.json index 52d4fd7bb..43719555c 100644 --- a/packages/react-form/tsconfig.json +++ b/packages/react-form/tsconfig.json @@ -7,5 +7,11 @@ "@tanstack/form-core": ["../form-core/src"] } }, - "include": ["src", "tests", "eslint.config.js", "vite.config.ts"] + "include": [ + "src", + "tests", + "benchmarks", + "eslint.config.js", + "vite.config.ts" + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9414cc9bd..56de31bbb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1257,7 +1257,7 @@ importers: version: 5.8.2 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.2)(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)) + version: 5.1.4(typescript@5.8.2)(vite@7.3.1(@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)) zone.js: specifier: 0.15.1 version: 0.15.1 @@ -1334,6 +1334,12 @@ importers: 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: + '@hookform/error-message': + specifier: ^2.0.1 + version: 2.0.1(react-dom@19.1.0(react@19.1.0))(react-hook-form@7.71.1(react@19.1.0))(react@19.1.0) + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.71.1(react@19.1.0)) '@types/react': specifier: ^19.0.7 version: 19.1.6 @@ -1346,15 +1352,27 @@ importers: eslint-plugin-react-compiler: specifier: 19.1.0-rc.2 version: 19.1.0-rc.2(eslint@9.36.0(jiti@2.6.1)) + formik: + specifier: ^2.4.6 + version: 2.4.9(@types/react@19.1.6)(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: ^7.54.2 + version: 7.71.1(react@19.1.0) vite: specifier: ^7.2.2 version: 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) + zod: + specifier: ^4.3.6 + version: 4.3.6 + zod-formik-adapter: + specifier: ^2.0.0 + version: 2.0.0(formik@2.4.9(@types/react@19.1.6)(react@19.1.0))(zod@4.3.6) packages/react-form-devtools: dependencies: @@ -1507,7 +1525,7 @@ importers: devDependencies: vite-plugin-solid: specifier: ^2.11.8 - version: 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)) + version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.3.1(@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)) packages/svelte-form: dependencies: @@ -1520,19 +1538,19 @@ 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)) + version: 5.1.1(svelte@5.41.1)(vite@7.3.1(@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)) '@testing-library/svelte': specifier: ^5.2.8 - version: 5.2.8(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))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(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)) + version: 5.2.8(svelte@5.41.1)(vite@7.3.1(@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))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(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)) svelte: specifier: ^5.39.4 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: @@ -2623,12 +2641,24 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.25.9': resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.17.6': resolution: {integrity: sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg==} engines: {node: '>=12'} @@ -2641,12 +2671,24 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.25.9': resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.17.6': resolution: {integrity: sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g==} engines: {node: '>=12'} @@ -2659,12 +2701,24 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.25.9': resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.17.6': resolution: {integrity: sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ==} engines: {node: '>=12'} @@ -2677,12 +2731,24 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.25.9': resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.17.6': resolution: {integrity: sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA==} engines: {node: '>=12'} @@ -2695,12 +2761,24 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.25.9': resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.17.6': resolution: {integrity: sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg==} engines: {node: '>=12'} @@ -2713,12 +2791,24 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.25.9': resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.17.6': resolution: {integrity: sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg==} engines: {node: '>=12'} @@ -2731,12 +2821,24 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.25.9': resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.17.6': resolution: {integrity: sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q==} engines: {node: '>=12'} @@ -2749,12 +2851,24 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.9': resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.17.6': resolution: {integrity: sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w==} engines: {node: '>=12'} @@ -2767,12 +2881,24 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.25.9': resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.17.6': resolution: {integrity: sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw==} engines: {node: '>=12'} @@ -2785,12 +2911,24 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.25.9': resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.17.6': resolution: {integrity: sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ==} engines: {node: '>=12'} @@ -2803,12 +2941,24 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.25.9': resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.17.6': resolution: {integrity: sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ==} engines: {node: '>=12'} @@ -2821,12 +2971,24 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.25.9': resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.17.6': resolution: {integrity: sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA==} engines: {node: '>=12'} @@ -2839,12 +3001,24 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.25.9': resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.17.6': resolution: {integrity: sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg==} engines: {node: '>=12'} @@ -2857,12 +3031,24 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.25.9': resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.17.6': resolution: {integrity: sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ==} engines: {node: '>=12'} @@ -2875,12 +3061,24 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.25.9': resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.17.6': resolution: {integrity: sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q==} engines: {node: '>=12'} @@ -2893,12 +3091,24 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.25.9': resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.17.6': resolution: {integrity: sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw==} engines: {node: '>=12'} @@ -2911,18 +3121,42 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.25.9': resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-arm64@0.25.9': resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.17.6': resolution: {integrity: sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A==} engines: {node: '>=12'} @@ -2935,18 +3169,42 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.9': resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.25.9': resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.17.6': resolution: {integrity: sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw==} engines: {node: '>=12'} @@ -2959,18 +3217,42 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.9': resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/openharmony-arm64@0.25.9': resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.17.6': resolution: {integrity: sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw==} engines: {node: '>=12'} @@ -2983,12 +3265,24 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.25.9': resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.17.6': resolution: {integrity: sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg==} engines: {node: '>=12'} @@ -3001,12 +3295,24 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.25.9': resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.17.6': resolution: {integrity: sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg==} engines: {node: '>=12'} @@ -3019,12 +3325,24 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.25.9': resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.17.6': resolution: {integrity: sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA==} engines: {node: '>=12'} @@ -3037,12 +3355,24 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.25.9': resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3139,6 +3469,18 @@ packages: '@gerrit0/mini-shiki@3.19.0': resolution: {integrity: sha512-ZSlWfLvr8Nl0T4iA3FF/8VH8HivYF82xQts2DY0tJxZd4wtXJ8AA0nmdW9lmO4hlrh3f9xNwEPtOgqETPqKwDA==} + '@hookform/error-message@2.0.1': + resolution: {integrity: sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-hook-form: ^7.0.0 + + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -4332,6 +4674,11 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.57.1': + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.52.3': resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} cpu: [arm64] @@ -4342,6 +4689,11 @@ packages: cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.57.1': + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.52.3': resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} cpu: [arm64] @@ -4352,6 +4704,11 @@ packages: cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.57.1': + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.52.3': resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} cpu: [x64] @@ -4362,6 +4719,11 @@ packages: cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.57.1': + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.52.3': resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} cpu: [arm64] @@ -4372,6 +4734,11 @@ packages: cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.57.1': + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.52.3': resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} cpu: [x64] @@ -4382,6 +4749,11 @@ packages: cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.57.1': + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} cpu: [arm] @@ -4392,6 +4764,11 @@ packages: cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.52.3': resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} cpu: [arm] @@ -4402,6 +4779,11 @@ packages: cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.52.3': resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} cpu: [arm64] @@ -4412,6 +4794,11 @@ packages: cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.57.1': + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.52.3': resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} cpu: [arm64] @@ -4422,6 +4809,11 @@ packages: cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.57.1': + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.52.3': resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} cpu: [loong64] @@ -4432,6 +4824,16 @@ packages: cpu: [loong64] os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.57.1': + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.57.1': + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.52.3': resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} cpu: [ppc64] @@ -4442,6 +4844,16 @@ packages: cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.52.3': resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} cpu: [riscv64] @@ -4452,6 +4864,11 @@ packages: cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.52.3': resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} cpu: [riscv64] @@ -4462,6 +4879,11 @@ packages: cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.57.1': + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.52.3': resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} cpu: [s390x] @@ -4472,6 +4894,11 @@ packages: cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.57.1': + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.52.3': resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} cpu: [x64] @@ -4482,6 +4909,11 @@ packages: cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.57.1': + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.52.3': resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} cpu: [x64] @@ -4492,6 +4924,16 @@ packages: cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.57.1': + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.57.1': + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} + cpu: [x64] + os: [openbsd] + '@rollup/rollup-openharmony-arm64@4.52.3': resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} cpu: [arm64] @@ -4502,6 +4944,11 @@ packages: cpu: [arm64] os: [openharmony] + '@rollup/rollup-openharmony-arm64@4.57.1': + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + cpu: [arm64] + os: [openharmony] + '@rollup/rollup-win32-arm64-msvc@4.52.3': resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} cpu: [arm64] @@ -4512,6 +4959,11 @@ packages: cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.57.1': + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.52.3': resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} cpu: [ia32] @@ -4522,6 +4974,11 @@ packages: cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.57.1': + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-gnu@4.52.3': resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} cpu: [x64] @@ -4532,6 +4989,11 @@ packages: cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-gnu@4.57.1': + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} + cpu: [x64] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.52.3': resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} cpu: [x64] @@ -4542,6 +5004,11 @@ packages: cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.57.1': + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + cpu: [x64] + os: [win32] + '@rollup/wasm-node@4.41.1': resolution: {integrity: sha512-70qfem+U3hAgwNgOlnUQiIdfKHLELUxsEWbFWg3aErPUvsyXYF1HALJBwoDgMUhRWyn+SqWVneDTnO/Kbey9hg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -4658,6 +5125,9 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@stylistic/eslint-plugin@5.5.0': resolution: {integrity: sha512-IeZF+8H0ns6prg4VrkhgL+yrvDXWDH2cKchrbh80ejG9dQgZWp10epHMbgRuQvgchLII/lfh6Xn3lu6+6L86Hw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5188,6 +5658,11 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + '@types/http-errors@2.0.4': resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} @@ -6408,6 +6883,10 @@ packages: deep-object-diff@1.1.9: resolution: {integrity: sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==} + deepmerge@2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -6674,11 +7153,21 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.25.9: resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} hasBin: true + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -7091,6 +7580,11 @@ packages: engines: {node: '>=18.3.0'} hasBin: true + formik@2.4.9: + resolution: {integrity: sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og==} + peerDependencies: + react: '>=16.8.0' + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -7970,6 +8464,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -9120,6 +9617,15 @@ packages: peerDependencies: react: ^19.1.0 + react-fast-compare@2.0.4: + resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} + + react-hook-form@7.71.1: + resolution: {integrity: sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -9359,8 +9865,13 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rou3@0.7.10: - resolution: {integrity: sha512-aoFj6f7MJZ5muJ+Of79nrhs9N3oLGqi2VEMe94Zbkjb6Wupha46EuoYgpWSOZlXww3bbd8ojgXTAA2mzimX5Ww==} + rollup@4.57.1: + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} @@ -10507,6 +11018,46 @@ packages: yaml: optional: true + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitefu@1.1.1: resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} peerDependencies: @@ -10842,6 +11393,12 @@ packages: zimmerframe@1.1.2: resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zod-formik-adapter@2.0.0: + resolution: {integrity: sha512-cLykLmQgmCmhx8C/mwR+rUNKRw6kEukejGRrByB+56zV1XtHF+xmOiQ2Lp2w5R0BdN3z42QKFekOI0Vy39vR8Q==} + peerDependencies: + formik: ^2.2.9 + zod: 4.x + zod-to-json-schema@3.24.6: resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} peerDependencies: @@ -10856,8 +11413,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.1.12: - resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} zone.js@0.15.1: resolution: {integrity: sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==} @@ -12361,88 +12918,148 @@ snapshots: '@esbuild/aix-ppc64@0.21.5': optional: true + '@esbuild/aix-ppc64@0.25.12': + optional: true + '@esbuild/aix-ppc64@0.25.9': optional: true + '@esbuild/aix-ppc64@0.27.3': + optional: true + '@esbuild/android-arm64@0.17.6': optional: true '@esbuild/android-arm64@0.21.5': optional: true + '@esbuild/android-arm64@0.25.12': + optional: true + '@esbuild/android-arm64@0.25.9': optional: true + '@esbuild/android-arm64@0.27.3': + optional: true + '@esbuild/android-arm@0.17.6': optional: true '@esbuild/android-arm@0.21.5': optional: true + '@esbuild/android-arm@0.25.12': + optional: true + '@esbuild/android-arm@0.25.9': optional: true + '@esbuild/android-arm@0.27.3': + optional: true + '@esbuild/android-x64@0.17.6': optional: true '@esbuild/android-x64@0.21.5': optional: true + '@esbuild/android-x64@0.25.12': + optional: true + '@esbuild/android-x64@0.25.9': optional: true + '@esbuild/android-x64@0.27.3': + optional: true + '@esbuild/darwin-arm64@0.17.6': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true + '@esbuild/darwin-arm64@0.25.12': + optional: true + '@esbuild/darwin-arm64@0.25.9': optional: true + '@esbuild/darwin-arm64@0.27.3': + optional: true + '@esbuild/darwin-x64@0.17.6': optional: true '@esbuild/darwin-x64@0.21.5': optional: true + '@esbuild/darwin-x64@0.25.12': + optional: true + '@esbuild/darwin-x64@0.25.9': optional: true + '@esbuild/darwin-x64@0.27.3': + optional: true + '@esbuild/freebsd-arm64@0.17.6': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true + '@esbuild/freebsd-arm64@0.25.12': + optional: true + '@esbuild/freebsd-arm64@0.25.9': optional: true + '@esbuild/freebsd-arm64@0.27.3': + optional: true + '@esbuild/freebsd-x64@0.17.6': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true + '@esbuild/freebsd-x64@0.25.12': + optional: true + '@esbuild/freebsd-x64@0.25.9': optional: true + '@esbuild/freebsd-x64@0.27.3': + optional: true + '@esbuild/linux-arm64@0.17.6': optional: true '@esbuild/linux-arm64@0.21.5': optional: true + '@esbuild/linux-arm64@0.25.12': + optional: true + '@esbuild/linux-arm64@0.25.9': optional: true + '@esbuild/linux-arm64@0.27.3': + optional: true + '@esbuild/linux-arm@0.17.6': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-arm@0.25.9': + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.9': + optional: true + + '@esbuild/linux-arm@0.27.3': optional: true '@esbuild/linux-ia32@0.17.6': @@ -12451,126 +13068,222 @@ snapshots: '@esbuild/linux-ia32@0.21.5': optional: true + '@esbuild/linux-ia32@0.25.12': + optional: true + '@esbuild/linux-ia32@0.25.9': optional: true + '@esbuild/linux-ia32@0.27.3': + optional: true + '@esbuild/linux-loong64@0.17.6': optional: true '@esbuild/linux-loong64@0.21.5': optional: true + '@esbuild/linux-loong64@0.25.12': + optional: true + '@esbuild/linux-loong64@0.25.9': optional: true + '@esbuild/linux-loong64@0.27.3': + optional: true + '@esbuild/linux-mips64el@0.17.6': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true + '@esbuild/linux-mips64el@0.25.12': + optional: true + '@esbuild/linux-mips64el@0.25.9': optional: true + '@esbuild/linux-mips64el@0.27.3': + optional: true + '@esbuild/linux-ppc64@0.17.6': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true + '@esbuild/linux-ppc64@0.25.12': + optional: true + '@esbuild/linux-ppc64@0.25.9': optional: true + '@esbuild/linux-ppc64@0.27.3': + optional: true + '@esbuild/linux-riscv64@0.17.6': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true + '@esbuild/linux-riscv64@0.25.12': + optional: true + '@esbuild/linux-riscv64@0.25.9': optional: true + '@esbuild/linux-riscv64@0.27.3': + optional: true + '@esbuild/linux-s390x@0.17.6': optional: true '@esbuild/linux-s390x@0.21.5': optional: true + '@esbuild/linux-s390x@0.25.12': + optional: true + '@esbuild/linux-s390x@0.25.9': optional: true + '@esbuild/linux-s390x@0.27.3': + optional: true + '@esbuild/linux-x64@0.17.6': optional: true '@esbuild/linux-x64@0.21.5': optional: true + '@esbuild/linux-x64@0.25.12': + optional: true + '@esbuild/linux-x64@0.25.9': optional: true + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + '@esbuild/netbsd-arm64@0.25.9': optional: true + '@esbuild/netbsd-arm64@0.27.3': + optional: true + '@esbuild/netbsd-x64@0.17.6': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true + '@esbuild/netbsd-x64@0.25.12': + optional: true + '@esbuild/netbsd-x64@0.25.9': optional: true + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + '@esbuild/openbsd-arm64@0.25.9': optional: true + '@esbuild/openbsd-arm64@0.27.3': + optional: true + '@esbuild/openbsd-x64@0.17.6': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true + '@esbuild/openbsd-x64@0.25.12': + optional: true + '@esbuild/openbsd-x64@0.25.9': optional: true + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + '@esbuild/openharmony-arm64@0.25.9': optional: true + '@esbuild/openharmony-arm64@0.27.3': + optional: true + '@esbuild/sunos-x64@0.17.6': optional: true '@esbuild/sunos-x64@0.21.5': optional: true + '@esbuild/sunos-x64@0.25.12': + optional: true + '@esbuild/sunos-x64@0.25.9': optional: true + '@esbuild/sunos-x64@0.27.3': + optional: true + '@esbuild/win32-arm64@0.17.6': optional: true '@esbuild/win32-arm64@0.21.5': optional: true + '@esbuild/win32-arm64@0.25.12': + optional: true + '@esbuild/win32-arm64@0.25.9': optional: true + '@esbuild/win32-arm64@0.27.3': + optional: true + '@esbuild/win32-ia32@0.17.6': optional: true '@esbuild/win32-ia32@0.21.5': optional: true + '@esbuild/win32-ia32@0.25.12': + optional: true + '@esbuild/win32-ia32@0.25.9': optional: true + '@esbuild/win32-ia32@0.27.3': + optional: true + '@esbuild/win32-x64@0.17.6': optional: true '@esbuild/win32-x64@0.21.5': optional: true + '@esbuild/win32-x64@0.25.12': + optional: true + '@esbuild/win32-x64@0.25.9': optional: true + '@esbuild/win32-x64@0.27.3': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.6.1))': dependencies: eslint: 9.36.0(jiti@2.6.1) @@ -12638,7 +13351,7 @@ snapshots: '@eslint-react/eff': 1.53.1 '@typescript-eslint/utils': 8.46.4(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.2) ts-pattern: 5.9.0 - zod: 4.1.12 + zod: 4.3.6 transitivePeerDependencies: - eslint - supports-color @@ -12650,7 +13363,7 @@ snapshots: '@eslint-react/kit': 1.53.1(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.2) '@typescript-eslint/utils': 8.46.4(eslint@9.36.0(jiti@2.6.1))(typescript@5.8.2) ts-pattern: 5.9.0 - zod: 4.1.12 + zod: 4.3.6 transitivePeerDependencies: - eslint - supports-color @@ -12740,6 +13453,17 @@ snapshots: '@shikijs/types': 3.19.0 '@shikijs/vscode-textmate': 10.0.2 + '@hookform/error-message@2.0.1(react-dom@19.1.0(react@19.1.0))(react-hook-form@7.71.1(react@19.1.0))(react@19.1.0)': + dependencies: + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-hook-form: 7.71.1(react@19.1.0) + + '@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.1.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.71.1(react@19.1.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -13899,132 +14623,207 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.52.5': optional: true + '@rollup/rollup-android-arm-eabi@4.57.1': + optional: true + '@rollup/rollup-android-arm64@4.52.3': optional: true '@rollup/rollup-android-arm64@4.52.5': optional: true + '@rollup/rollup-android-arm64@4.57.1': + optional: true + '@rollup/rollup-darwin-arm64@4.52.3': optional: true '@rollup/rollup-darwin-arm64@4.52.5': optional: true + '@rollup/rollup-darwin-arm64@4.57.1': + optional: true + '@rollup/rollup-darwin-x64@4.52.3': optional: true '@rollup/rollup-darwin-x64@4.52.5': optional: true + '@rollup/rollup-darwin-x64@4.57.1': + optional: true + '@rollup/rollup-freebsd-arm64@4.52.3': optional: true '@rollup/rollup-freebsd-arm64@4.52.5': optional: true + '@rollup/rollup-freebsd-arm64@4.57.1': + optional: true + '@rollup/rollup-freebsd-x64@4.52.3': optional: true '@rollup/rollup-freebsd-x64@4.52.5': optional: true + '@rollup/rollup-freebsd-x64@4.57.1': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': optional: true '@rollup/rollup-linux-arm-gnueabihf@4.52.5': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.52.3': optional: true '@rollup/rollup-linux-arm-musleabihf@4.52.5': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.57.1': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.52.3': optional: true '@rollup/rollup-linux-arm64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-arm64-gnu@4.57.1': + optional: true + '@rollup/rollup-linux-arm64-musl@4.52.3': optional: true '@rollup/rollup-linux-arm64-musl@4.52.5': optional: true + '@rollup/rollup-linux-arm64-musl@4.57.1': + optional: true + '@rollup/rollup-linux-loong64-gnu@4.52.3': optional: true '@rollup/rollup-linux-loong64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-loong64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.1': + optional: true + '@rollup/rollup-linux-ppc64-gnu@4.52.3': optional: true '@rollup/rollup-linux-ppc64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-ppc64-gnu@4.57.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.1': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.52.3': optional: true '@rollup/rollup-linux-riscv64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.57.1': + optional: true + '@rollup/rollup-linux-riscv64-musl@4.52.3': optional: true '@rollup/rollup-linux-riscv64-musl@4.52.5': optional: true + '@rollup/rollup-linux-riscv64-musl@4.57.1': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.52.3': optional: true '@rollup/rollup-linux-s390x-gnu@4.52.5': optional: true + '@rollup/rollup-linux-s390x-gnu@4.57.1': + optional: true + '@rollup/rollup-linux-x64-gnu@4.52.3': optional: true '@rollup/rollup-linux-x64-gnu@4.52.5': optional: true + '@rollup/rollup-linux-x64-gnu@4.57.1': + optional: true + '@rollup/rollup-linux-x64-musl@4.52.3': optional: true '@rollup/rollup-linux-x64-musl@4.52.5': optional: true + '@rollup/rollup-linux-x64-musl@4.57.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.1': + optional: true + '@rollup/rollup-openharmony-arm64@4.52.3': optional: true '@rollup/rollup-openharmony-arm64@4.52.5': optional: true + '@rollup/rollup-openharmony-arm64@4.57.1': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.52.3': optional: true '@rollup/rollup-win32-arm64-msvc@4.52.5': optional: true + '@rollup/rollup-win32-arm64-msvc@4.57.1': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.52.3': optional: true '@rollup/rollup-win32-ia32-msvc@4.52.5': optional: true + '@rollup/rollup-win32-ia32-msvc@4.57.1': + optional: true + '@rollup/rollup-win32-x64-gnu@4.52.3': optional: true '@rollup/rollup-win32-x64-gnu@4.52.5': optional: true + '@rollup/rollup-win32-x64-gnu@4.57.1': + optional: true + '@rollup/rollup-win32-x64-msvc@4.52.3': optional: true '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true + '@rollup/rollup-win32-x64-msvc@4.57.1': + optional: true + '@rollup/wasm-node@4.41.1': dependencies: '@types/estree': 1.0.7 @@ -14168,6 +14967,8 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@standard-schema/utils@0.3.0': {} + '@stylistic/eslint-plugin@5.5.0(eslint@9.36.0(jiti@2.6.1))': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.1)) @@ -14182,14 +14983,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 @@ -14202,6 +15003,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.41.1)(vite@7.3.1(@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)))(svelte@5.41.1)(vite@7.3.1(@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))': + dependencies: + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.41.1)(vite@7.3.1(@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)) + debug: 4.4.3 + svelte: 5.41.1 + vite: 7.3.1(@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) + transitivePeerDependencies: + - supports-color + '@sveltejs/vite-plugin-svelte@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))': dependencies: '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@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)))(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)) @@ -14215,6 +15025,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.41.1)(vite@7.3.1(@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))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.41.1)(vite@7.3.1(@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)))(svelte@5.41.1)(vite@7.3.1(@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)) + debug: 4.4.3 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.19 + svelte: 5.41.1 + vite: 7.3.1(@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) + vitefu: 1.1.1(vite@7.3.1(@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)) + transitivePeerDependencies: + - supports-color + '@svitejs/changesets-changelog-github-compact@1.2.0(encoding@0.1.13)': dependencies: '@changesets/get-github-info': 0.6.0(encoding@0.1.13) @@ -14530,7 +15353,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 @@ -14726,12 +15549,12 @@ snapshots: '@types/react': 19.1.6 '@types/react-dom': 19.1.5(@types/react@19.1.6) - '@testing-library/svelte@5.2.8(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))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(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))': + '@testing-library/svelte@5.2.8(svelte@5.41.1)(vite@7.3.1(@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))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(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))': dependencies: '@testing-library/dom': 10.4.0 svelte: 5.41.1 optionalDependencies: - 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: 7.3.1(@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) vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(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) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': @@ -14878,6 +15701,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/hoist-non-react-statics@3.3.7(@types/react@19.1.6)': + dependencies: + '@types/react': 19.1.6 + hoist-non-react-statics: 3.3.2 + '@types/http-errors@2.0.4': {} '@types/http-proxy@1.17.16': @@ -16316,6 +17144,8 @@ snapshots: deep-object-diff@1.1.9: {} + deepmerge@2.2.1: {} + deepmerge@4.3.1: {} default-browser-id@5.0.0: {} @@ -16600,6 +17430,35 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.25.9: optionalDependencies: '@esbuild/aix-ppc64': 0.25.9 @@ -16629,6 +17488,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.9 '@esbuild/win32-x64': 0.25.9 + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -17194,6 +18082,20 @@ snapshots: dependencies: fd-package-json: 2.0.0 + formik@2.4.9(@types/react@19.1.6)(react@19.1.0): + dependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@19.1.6) + deepmerge: 2.2.1 + hoist-non-react-statics: 3.3.2 + lodash: 4.17.21 + lodash-es: 4.17.23 + react: 19.1.0 + react-fast-compare: 2.0.4 + tiny-warning: 1.0.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/react' + forwarded@0.2.0: {} fraction.js@4.3.7: {} @@ -17338,7 +18240,7 @@ snapshots: dependencies: cookie-es: 2.0.0 fetchdts: 0.1.7 - rou3: 0.7.10 + rou3: 0.7.12 srvx: 0.8.16 handle-thing@2.0.1: {} @@ -17960,7 +18862,7 @@ snapshots: smol-toml: 1.5.2 strip-json-comments: 5.0.3 typescript: 5.8.2 - zod: 4.1.12 + zod: 4.3.6 kolorist@1.8.0: {} @@ -18083,6 +18985,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.23: {} + lodash.camelcase@4.3.0: {} lodash.debounce@4.0.8: {} @@ -19544,6 +20448,12 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-fast-compare@2.0.4: {} + + react-hook-form@7.71.1(react@19.1.0): + dependencies: + react: 19.1.0 + react-is@16.13.1: {} react-is@17.0.2: {} @@ -19850,7 +20760,38 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 - rou3@0.7.10: {} + rollup@4.57.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.1 + '@rollup/rollup-android-arm64': 4.57.1 + '@rollup/rollup-darwin-arm64': 4.57.1 + '@rollup/rollup-darwin-x64': 4.57.1 + '@rollup/rollup-freebsd-arm64': 4.57.1 + '@rollup/rollup-freebsd-x64': 4.57.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 + '@rollup/rollup-linux-arm64-musl': 4.57.1 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 + '@rollup/rollup-linux-loong64-musl': 4.57.1 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 + '@rollup/rollup-linux-x64-gnu': 4.57.1 + '@rollup/rollup-linux-x64-musl': 4.57.1 + '@rollup/rollup-openbsd-x64': 4.57.1 + '@rollup/rollup-openharmony-arm64': 4.57.1 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 + '@rollup/rollup-win32-x64-gnu': 4.57.1 + '@rollup/rollup-win32-x64-msvc': 4.57.1 + fsevents: 2.3.3 + + rou3@0.7.12: {} router@2.2.0: dependencies: @@ -20396,7 +21337,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 @@ -20404,16 +21345,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: @@ -20485,6 +21426,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 @@ -20599,7 +21552,7 @@ snapshots: tsx@4.19.4: dependencies: - esbuild: 0.25.9 + esbuild: 0.25.12 get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 @@ -20670,7 +21623,8 @@ snapshots: typescript@5.9.2: {} - typescript@5.9.3: {} + typescript@5.9.3: + optional: true uc.micro@2.1.0: {} @@ -20985,6 +21939,21 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.9)(vite@7.3.1(@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)): + dependencies: + '@babel/core': 7.28.3 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.6(@babel/core@7.28.3) + merge-anything: 5.1.7 + solid-js: 1.9.9 + solid-refresh: 0.6.3(solid-js@1.9.9) + vite: 7.3.1(@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) + vitefu: 1.1.1(vite@7.3.1(@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)) + optionalDependencies: + '@testing-library/jest-dom': 6.9.1 + transitivePeerDependencies: + - supports-color + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(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)): dependencies: debug: 4.4.3 @@ -20996,6 +21965,17 @@ snapshots: - supports-color - typescript + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@7.3.1(@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)): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.8.2) + optionalDependencies: + vite: 7.3.1(@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) + transitivePeerDependencies: + - supports-color + - typescript + vite@5.4.19(@types/node@24.1.0)(less@4.4.0)(sass@1.90.0)(sugarss@5.0.1(postcss@8.5.6))(terser@5.43.1): dependencies: esbuild: 0.21.5 @@ -21047,10 +22027,33 @@ snapshots: tsx: 4.19.4 yaml: 2.8.1 + vite@7.3.1(@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): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.1.0 + fsevents: 2.3.3 + 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 + vitefu@1.1.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)): optionalDependencies: 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) + vitefu@1.1.1(vite@7.3.1(@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)): + optionalDependencies: + vite: 7.3.1(@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) + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.1.0)(jiti@2.6.1)(jsdom@27.3.0(postcss@8.5.6))(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): dependencies: '@types/chai': 5.2.2 @@ -21238,6 +22241,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 @@ -21428,6 +22464,11 @@ snapshots: zimmerframe@1.1.2: {} + zod-formik-adapter@2.0.0(formik@2.4.9(@types/react@19.1.6)(react@19.1.0))(zod@4.3.6): + dependencies: + formik: 2.4.9(@types/react@19.1.6)(react@19.1.0) + zod: 4.3.6 + zod-to-json-schema@3.24.6(zod@3.25.76): dependencies: zod: 3.25.76 @@ -21438,7 +22479,7 @@ snapshots: zod@3.25.76: {} - zod@4.1.12: {} + zod@4.3.6: {} zone.js@0.15.1: {} From 4fa90998c82d6a00867f54189f875a4a00fc2c40 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 17:12:11 -0800 Subject: [PATCH 05/31] chore: make benchmarks more accurate --- .../tests/onchange-detection.bench.tsx | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/react-form/tests/onchange-detection.bench.tsx b/packages/react-form/tests/onchange-detection.bench.tsx index 224438ac6..efdbd2dbb 100644 --- a/packages/react-form/tests/onchange-detection.bench.tsx +++ b/packages/react-form/tests/onchange-detection.bench.tsx @@ -13,20 +13,23 @@ import type { FieldProps } from 'formik/dist/Field' const arr = Array.from({ length: 100 }, (_, i) => i) -const validators = { - onChange: z.number().min(3, 'Must be at least three'), -} +const validator = z.object({ + num: z.array(z.number().min(3, 'Must be at least three')), +}) function TanStackFormOnChangeBenchmark() { const form = useTanStackForm({ defaultValues: { num: arr }, + validators: { + onChange: validator, + }, }) return ( <> {arr.map((_num, i) => { return ( - + {(field) => { return (
@@ -60,11 +63,7 @@ function FormikOnChangeBenchmark() { initialValues={{ num: arr, }} - validationSchema={toFormikValidationSchema( - z.object({ - num: z.array(z.number().min(3, 'Must be at least three')), - }), - )} + validationSchema={toFormikValidationSchema(validator)} onSubmit={() => {}} > {() => ( @@ -102,11 +101,7 @@ function ReactHookFormOnChangeBenchmark() { num: arr, }, mode: 'onChange', - resolver: zodResolver( - z.object({ - num: z.array(z.number().min(3, 'Must be at least three')), - }), - ), + resolver: zodResolver(validator), }) return ( @@ -134,11 +129,7 @@ function ReactHookFormHeadlessOnChangeBenchmark() { num: arr, }, mode: 'onChange', - resolver: zodResolver( - z.object({ - num: z.array(z.number().min(3, 'Must be at least three')), - }), - ), + resolver: zodResolver(validator), }) return ( From 003f341710b6ab2a4b36a072a67067a3ba86d593 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 18:21:49 -0800 Subject: [PATCH 06/31] chore: per-field stores --- packages/form-core/benchmark.ts | 223 +++++++++++++++++++++++++++++ packages/form-core/src/FieldApi.ts | 68 ++++++++- packages/form-core/src/FormApi.ts | 168 +++++++++++++++++++++- 3 files changed, 449 insertions(+), 10 deletions(-) create mode 100644 packages/form-core/benchmark.ts diff --git a/packages/form-core/benchmark.ts b/packages/form-core/benchmark.ts new file mode 100644 index 000000000..ff08dacb1 --- /dev/null +++ b/packages/form-core/benchmark.ts @@ -0,0 +1,223 @@ +/** + * Benchmark: Measures how FormApi + FieldApi perform under load + * + * Scenarios: + * 1. Many fields, single field change: Create N fields, change one field, measure recomputations + * 2. Many fields, sequential field changes: Create N fields, change each sequentially + * 3. Subscription count: How many subscribers fire when a single field changes + */ + +import { FormApi } from './src/FormApi' +import { FieldApi } from './src/FieldApi' + +const FIELD_COUNTS = [10, 50, 100, 500] +const ITERATIONS = 100 + +interface BenchResult { + scenario: string + fieldCount: number + totalMs: number + avgMs: number + subscriberCallCount: number +} + +function createFormWithFields(fieldCount: number) { + const defaultValues: Record = {} + for (let i = 0; i < fieldCount; i++) { + defaultValues[`field_${i}`] = `value_${i}` + } + + const form = new FormApi({ + defaultValues, + }) + + const fields: FieldApi[] = [] + + for (let i = 0; i < fieldCount; i++) { + const field = new FieldApi({ + form, + name: `field_${i}`, + } as any) + field.mount() + fields.push(field) + } + + return { form, fields } +} + +// Scenario 1: Single field change with N fields registered +function benchSingleFieldChange(fieldCount: number): BenchResult { + const { form, fields } = createFormWithFields(fieldCount) + + let subscriberCallCount = 0 + // Subscribe to all fields to mimic real UI + const unsubs = fields.map((field) => + field.store.subscribe(() => { + subscriberCallCount++ + }), + ) + + // Warmup + for (let i = 0; i < 5; i++) { + fields[0]!.setValue(`warmup_${i}`) + } + subscriberCallCount = 0 + + const start = performance.now() + for (let i = 0; i < ITERATIONS; i++) { + fields[0]!.setValue(`new_value_${i}`) + } + const totalMs = performance.now() - start + + unsubs.forEach((u) => u.unsubscribe()) + + return { + scenario: 'single_field_change', + fieldCount, + totalMs, + avgMs: totalMs / ITERATIONS, + subscriberCallCount, + } +} + +// Scenario 2: Change each field once sequentially +function benchSequentialFieldChanges(fieldCount: number): BenchResult { + const { form, fields } = createFormWithFields(fieldCount) + + let subscriberCallCount = 0 + const unsubs = fields.map((field) => + field.store.subscribe(() => { + subscriberCallCount++ + }), + ) + + // Warmup + fields[0]!.setValue('warmup') + subscriberCallCount = 0 + + const start = performance.now() + for (let i = 0; i < fieldCount; i++) { + fields[i]!.setValue(`updated_${i}`) + } + const totalMs = performance.now() - start + + unsubs.forEach((u) => u.unsubscribe()) + + return { + scenario: 'sequential_field_changes', + fieldCount, + totalMs, + avgMs: totalMs / fieldCount, + subscriberCallCount, + } +} + +// Scenario 3: Measure store recomputations per single change +function benchStoreRecomputations(fieldCount: number): BenchResult { + const { form, fields } = createFormWithFields(fieldCount) + + let subscriberCallCount = 0 + const unsubs = fields.map((field) => + field.store.subscribe(() => { + subscriberCallCount++ + }), + ) + + // Single field change - measure how many subscribers fire + subscriberCallCount = 0 + fields[0]!.setValue('trigger_change') + + const fireCount = subscriberCallCount + + unsubs.forEach((u) => u.unsubscribe()) + + return { + scenario: 'store_recomputations_per_change', + fieldCount, + totalMs: 0, + avgMs: 0, + subscriberCallCount: fireCount, + } +} + +// Scenario 4: Form store subscription count per field change +function benchFormStoreSubscriptions(fieldCount: number): BenchResult { + const { form, fields } = createFormWithFields(fieldCount) + + let formStoreCallCount = 0 + const formUnsub = form.store.subscribe(() => { + formStoreCallCount++ + }) + + // Single field change + formStoreCallCount = 0 + fields[0]!.setValue('trigger_change') + + formUnsub.unsubscribe() + + return { + scenario: 'form_store_subs_per_change', + fieldCount, + totalMs: 0, + avgMs: 0, + subscriberCallCount: formStoreCallCount, + } +} + +// Run all benchmarks +console.log('=== TanStack Form Performance Benchmark ===\n') + +const results: BenchResult[] = [] + +for (const count of FIELD_COUNTS) { + console.log(`--- ${count} fields ---`) + + const r1 = benchSingleFieldChange(count) + console.log( + ` Single field change (${ITERATIONS}x): ${r1.totalMs.toFixed(2)}ms total, ${r1.avgMs.toFixed(3)}ms avg, ${r1.subscriberCallCount} subscriber calls`, + ) + results.push(r1) + + const r2 = benchSequentialFieldChanges(count) + console.log( + ` Sequential changes: ${r2.totalMs.toFixed(2)}ms total, ${r2.avgMs.toFixed(3)}ms avg per field, ${r2.subscriberCallCount} subscriber calls`, + ) + results.push(r2) + + const r3 = benchStoreRecomputations(count) + console.log( + ` Subscriber fires per single change: ${r3.subscriberCallCount} (of ${count} fields)`, + ) + results.push(r3) + + const r4 = benchFormStoreSubscriptions(count) + console.log( + ` Form store fires per field change: ${r4.subscriberCallCount}`, + ) + results.push(r4) + + console.log() +} + +// Summary table +console.log('\n=== Summary: Subscriber calls per single field change ===') +console.log('Fields | Subscribers Fired | Expected (ideal)') +for (const count of FIELD_COUNTS) { + const r = results.find( + (r) => + r.scenario === 'store_recomputations_per_change' && + r.fieldCount === count, + ) + // Ideal: only 1 subscriber should fire (the changed field) + console.log(`${count.toString().padStart(6)} | ${r!.subscriberCallCount.toString().padStart(17)} | 1`) +} + +console.log('\n=== Summary: Time for single field change (ms avg) ===') +console.log('Fields | Avg time (ms)') +for (const count of FIELD_COUNTS) { + const r = results.find( + (r) => + r.scenario === 'single_field_change' && r.fieldCount === count, + ) + console.log(`${count.toString().padStart(6)} | ${r!.avgMs.toFixed(3)}`) +} diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 6cad062a1..2ccefcc8c 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -10,6 +10,7 @@ import { getAsyncValidatorArray, getBy, getSyncValidatorArray, + isNonEmptyArray, mergeOpts, } from './utils' import { defaultValidationLogic } from './ValidationLogic' @@ -1167,6 +1168,10 @@ export class FieldApi< formListeners: {} as Record, } + let prevMetaBase: any = undefined + let prevRawValue: any = undefined + let cachedMeta: any = undefined + this.store = createStore( ( prevVal: @@ -1195,15 +1200,66 @@ export class FieldApi< > | undefined, ) => { - // Temp hack to subscribe to form.store - this.form.store.get() + // Subscribe to per-field store for fine-grained reactivity (O(1) instead of O(N)) + const perFieldStore = this.form._getOrCreatePerFieldStore( + this.name as string, + opts.defaultMeta as any, + ) + const { value: rawValue, metaBase } = perFieldStore.get() + + // Compute derived meta with caching + let meta: any + if (metaBase === prevMetaBase && rawValue === prevRawValue && cachedMeta) { + // Nothing changed, reuse cached meta + meta = cachedMeta + } else { + // Recompute errors only if errorMap changed + let fieldErrors = cachedMeta?.errors ?? [] + if (!prevMetaBase || metaBase.errorMap !== prevMetaBase.errorMap) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + fieldErrors = Object.values(metaBase.errorMap ?? {}).filter( + (val: any) => val !== undefined, + ) + if (!this.options.disableErrorFlat) { + fieldErrors = fieldErrors.flat(1) + } + } + + const isFieldValid = !isNonEmptyArray(fieldErrors) + const isFieldPristine = !metaBase.isDirty + const isDefaultValue = + evaluate( + rawValue, + getBy(this.form.options.defaultValues, this.name), + ) || + evaluate(rawValue, this.options.defaultValue) + + // Check if derived values actually changed - preserve reference if not + if ( + cachedMeta && + cachedMeta.isPristine === isFieldPristine && + cachedMeta.isValid === isFieldValid && + cachedMeta.isDefaultValue === isDefaultValue && + cachedMeta.errors === fieldErrors && + metaBase === prevMetaBase + ) { + meta = cachedMeta + } else { + meta = { + ...metaBase, + errors: fieldErrors, + isPristine: isFieldPristine, + isValid: isFieldValid, + isDefaultValue: isDefaultValue, + } + } - const meta = this.form.getFieldMeta(this.name) ?? { - ...defaultFieldMeta, - ...opts.defaultMeta, + prevMetaBase = metaBase + prevRawValue = rawValue + cachedMeta = meta } - let value = this.form.getFieldValue(this.name) + let value = rawValue if ( !meta.isTouched && (value as unknown) === undefined && diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index e4d2466a8..8a98f5b44 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -926,6 +926,14 @@ export class FormApi< */ fieldInfo: Record, FieldInfo> = {} as any + /** + * @private + * Per-field mutable stores that hold { value, metaBase } for each registered field. + * FieldApi instances subscribe to these instead of the full form store for O(1) reactivity. + */ + _perFieldStores: Record> = + {} as any + get state() { return this.store.state } @@ -1335,6 +1343,95 @@ export class FormApi< return this._formId } + /** + * @private + * Gets or creates a per-field mutable store for the given field path. + * FieldApi instances subscribe to these for fine-grained reactivity. + */ + _getOrCreatePerFieldStore = ( + field: string, + overrideDefaultMeta?: Partial, + ): Store<{ value: any; metaBase: AnyFieldMetaBase }> => { + if (!this._perFieldStores[field]) { + const baseMeta = this.baseStore.state.fieldMetaBase[ + field as keyof typeof this.baseStore.state.fieldMetaBase + ] as AnyFieldMetaBase | undefined + this._perFieldStores[field] = createStore({ + value: getBy(this.baseStore.state.values, field), + metaBase: baseMeta ?? { + ...defaultFieldMeta, + ...overrideDefaultMeta, + }, + }) + } + return this._perFieldStores[field]! + } + + /** + * @private + * Notifies per-field value stores for the changed field AND any related parent/child fields. + * Parent fields are affected because immutable updates create new references up the tree. + * Child fields are affected because changing a parent value changes descendant values. + */ + _notifyRelatedPerFieldValueStores = (field: string): void => { + const newValues = this.baseStore.state.values + + // Update the specific field + const store = this._perFieldStores[field] + if (store) { + store.setState((prev) => ({ + ...prev, + value: getBy(newValues, field), + })) + } + + // Update parent paths (e.g., for 'items[0].name', update 'items' and 'items[0]') + let parentPath = '' + for (let i = 0; i < field.length; i++) { + const char = field[i] + if (char === '.' || char === '[') { + const parentStore = this._perFieldStores[parentPath] + if (parentStore) { + parentStore.setState((prev) => ({ + ...prev, + value: getBy(newValues, parentPath), + })) + } + } + parentPath += char + } + + // Update child paths (e.g., for 'items', update 'items[0]', 'items[0].name', etc.) + const fieldDot = field + '.' + const fieldBracket = field + '[' + for (const key of Object.keys(this._perFieldStores)) { + if (key.startsWith(fieldDot) || key.startsWith(fieldBracket)) { + this._perFieldStores[key]!.setState((prev) => ({ + ...prev, + value: getBy(newValues, key), + })) + } + } + } + + /** + * @private + * Updates ALL per-field stores from the current baseStore state. + * Used after bulk operations like reset() and update(). + */ + _syncAllPerFieldStores = (): void => { + const state = this.baseStore.state + for (const field of Object.keys(this._perFieldStores)) { + this._perFieldStores[field]!.setState(() => ({ + value: getBy(state.values, field), + metaBase: + (state.fieldMetaBase[ + field as keyof typeof state.fieldMetaBase + ] as AnyFieldMetaBase) ?? { ...defaultFieldMeta }, + })) + } + } + /** * @private */ @@ -1487,6 +1584,9 @@ export class FormApi< ) // }) + // Sync per-field stores after bulk state update + this._syncAllPerFieldStores() + formEventClient.emit('form-api', { id: this._formId, state: this.store.state, @@ -1522,6 +1622,9 @@ export class FormApi< fieldMetaBase, }), ) + + // Sync all per-field stores after reset + this._syncAllPerFieldStores() } /** @@ -2242,18 +2345,29 @@ export class FormApi< field: TField, updater: Updater, ) => { + let newMeta: AnyFieldMetaBase this.baseStore.setState((prev) => { + newMeta = functionalUpdate( + updater, + prev.fieldMetaBase[field] as never, + ) return { ...prev, fieldMetaBase: { ...prev.fieldMetaBase, - [field]: functionalUpdate( - updater, - prev.fieldMetaBase[field] as never, - ), + [field]: newMeta, }, } }) + + // Also update the per-field store for fine-grained reactivity + const perFieldStore = this._perFieldStores[field as string] + if (perFieldStore) { + perFieldStore.setState((prev) => ({ + ...prev, + metaBase: newMeta!, + })) + } } /** @@ -2304,6 +2418,9 @@ export class FormApi< values: setBy(prev.values, field, updater), } }) + + // Notify per-field value stores for this field and related parent/child fields + this._notifyRelatedPerFieldValueStores(field as string) // }) if (!dontRunListeners) { @@ -2334,6 +2451,40 @@ export class FormApi< return newState }) + + // Clean up per-field stores for deleted fields: + // First, reset them to default state so any FieldApi instances still referencing them get updated. + // Then notify parent/related per-field stores whose values may have changed. + fieldsToDelete.forEach((f) => { + const perFieldStore = this._perFieldStores[f as string] + if (perFieldStore) { + perFieldStore.setState(() => ({ + value: undefined, + metaBase: { ...defaultFieldMeta }, + })) + } + delete this._perFieldStores[f as string] + }) + + // Notify parent per-field stores that their values may have changed + const newValues = this.baseStore.state.values + for (const f of fieldsToDelete) { + const fieldStr = f as string + let parentPath = '' + for (let i = 0; i < fieldStr.length; i++) { + const char = fieldStr[i] + if (char === '.' || char === '[') { + const parentStore = this._perFieldStores[parentPath] + if (parentStore) { + parentStore.setState((prev) => ({ + ...prev, + value: getBy(newValues, parentPath), + })) + } + } + parentPath += char + } + } } /** @@ -2569,6 +2720,15 @@ export class FormApi< : prev.values, } }) + + // Update per-field store for the reset field + const perFieldStore = this._perFieldStores[field as string] + if (perFieldStore) { + perFieldStore.setState(() => ({ + value: getBy(this.baseStore.state.values, field as string), + metaBase: { ...defaultFieldMeta }, + })) + } } /** From 6f03895a27abe22eec5bd7cd954edde1ecea6a67 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 18:33:10 -0800 Subject: [PATCH 07/31] chore: faster again --- packages/form-core/src/FormApi.ts | 47 ++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 8a98f5b44..089a33cc3 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1779,7 +1779,7 @@ export class FormApi< const rawError = this.runValidator({ validate: validateObj.validate, value: { - value: this.state.values, + value: this.baseStore.state.values, formApi: this, validationSource: 'form', }, @@ -1791,7 +1791,7 @@ export class FormApi< const errorMapKey = getErrorMapKey(validateObj.cause) const allFieldsToProcess = new Set([ - ...Object.keys(this.state.fieldMeta), + ...Object.keys(this.baseStore.state.fieldMetaBase), ...Object.keys(fieldErrors || {}), ] as DeepKeys[]) @@ -1803,11 +1803,11 @@ export class FormApi< continue } - const fieldMeta = this.getFieldMeta(field) ?? defaultFieldMeta + const fieldMetaBase = this._getFieldMetaBase(field) ?? defaultFieldMeta const { errorMap: currentErrorMap, errorSourceMap: currentErrorMapSource, - } = fieldMeta + } = fieldMetaBase const newFormValidatorError = fieldErrors?.[field] @@ -1847,7 +1847,7 @@ export class FormApi< } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.state.errorMap?.[errorMapKey] !== formError) { + if (this.baseStore.state.errorMap?.[errorMapKey] !== formError) { this.baseStore.setState((prev) => ({ ...prev, errorMap: { @@ -1869,7 +1869,7 @@ export class FormApi< const submitErrKey = getErrorMapKey('submit') if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.state.errorMap?.[submitErrKey] && + this.baseStore.state.errorMap?.[submitErrKey] && cause !== 'submit' && !hasErrored ) { @@ -1889,7 +1889,7 @@ export class FormApi< const serverErrKey = getErrorMapKey('server') if ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.state.errorMap?.[serverErrKey] && + this.baseStore.state.errorMap?.[serverErrKey] && cause !== 'server' && !hasErrored ) { @@ -1931,8 +1931,8 @@ export class FormApi< validationLogic: this.options.validationLogic || defaultValidationLogic, }) - if (!this.state.isFormValidating) { - this.baseStore.setState((prev) => ({ ...prev, isFormValidating: true })) + if (!this.baseStore.state.isValidating) { + this.baseStore.setState((prev) => ({ ...prev, isValidating: true })) } /** @@ -1948,12 +1948,12 @@ export class FormApi< for (const validateObj of validates) { if (!validateObj.validate) continue const key = getErrorMapKey(validateObj.cause) - const fieldValidatorMeta = this.state.validationMetaMap[key] + const fieldValidatorMeta = this.baseStore.state.validationMetaMap[key] fieldValidatorMeta?.lastAbortController.abort() const controller = new AbortController() - this.state.validationMetaMap[key] = { + this.baseStore.state.validationMetaMap[key] = { lastAbortController: controller, } @@ -1972,7 +1972,7 @@ export class FormApi< await this.runValidator({ validate: validateObj.validate!, value: { - value: this.state.values, + value: this.baseStore.state.values, formApi: this, validationSource: 'form', signal: controller.signal, @@ -2002,19 +2002,19 @@ export class FormApi< const errorMapKey = getErrorMapKey(validateObj.cause) for (const field of Object.keys( - this.state.fieldMeta, + this.baseStore.state.fieldMetaBase, ) as DeepKeys[]) { if (this.baseStore.state.fieldMetaBase[field] === undefined) { continue } - const fieldMeta = this.getFieldMeta(field) - if (!fieldMeta) continue + const fieldMetaBase = this._getFieldMetaBase(field) + if (!fieldMetaBase) continue const { errorMap: currentErrorMap, errorSourceMap: currentErrorMapSource, - } = fieldMeta + } = fieldMetaBase const newFormValidatorError = fieldErrorsFromFormValidators?.[field] @@ -2307,7 +2307,7 @@ export class FormApi< */ getFieldValue = >( field: TField, - ): DeepValue => getBy(this.state.values, field) + ): DeepValue => getBy(this.baseStore.state.values, field) /** * Gets the metadata of the specified field. @@ -2318,6 +2318,19 @@ export class FormApi< return this.state.fieldMeta[field] } + /** + * @private + * Gets field meta base directly from baseStore without triggering O(N) derived store. + * Used in validation hot paths. + */ + _getFieldMetaBase = >( + field: TField, + ): AnyFieldMetaBase | undefined => { + return this.baseStore.state.fieldMetaBase[ + field as keyof typeof this.baseStore.state.fieldMetaBase + ] as AnyFieldMetaBase | undefined + } + /** * Gets the field info of the specified field. */ From bfc348db41cdb0fea1d73d145f72911bc578bd5a Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 18:40:36 -0800 Subject: [PATCH 08/31] chore: another --- packages/react-form/src/useField.tsx | 81 ++++++++++++---------------- 1 file changed, 35 insertions(+), 46 deletions(-) diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx index 2a3f546cf..dd40a1603 100644 --- a/packages/react-form/src/useField.tsx +++ b/packages/react-form/src/useField.tsx @@ -223,37 +223,32 @@ export function useField< // For array mode, only track length changes to avoid re-renders when child properties change // See: https://github.com/TanStack/form/issues/1925 - const reactiveStateValue = useStore( + // Consolidate all field state subscriptions into a single useStore call + // to reduce subscription overhead (1 subscription instead of 7) + const isArrayMode = opts.mode === 'array' + const reactiveState = useStore( fieldApi.store, - (opts.mode === 'array' - ? (state) => Object.keys((state.value as unknown) ?? []).length - : (state) => state.value) as ( - state: typeof fieldApi.state, - ) => TData | number, - ) - const reactiveMetaIsTouched = useStore( - fieldApi.store, - (state) => state.meta.isTouched, - ) - const reactiveMetaIsBlurred = useStore( - fieldApi.store, - (state) => state.meta.isBlurred, - ) - const reactiveMetaIsDirty = useStore( - fieldApi.store, - (state) => state.meta.isDirty, - ) - const reactiveMetaErrorMap = useStore( - fieldApi.store, - (state) => state.meta.errorMap, - ) - const reactiveMetaErrorSourceMap = useStore( - fieldApi.store, - (state) => state.meta.errorSourceMap, - ) - const reactiveMetaIsValidating = useStore( - fieldApi.store, - (state) => state.meta.isValidating, + (state) => { + return { + value: isArrayMode + ? Object.keys((state.value as unknown) ?? []).length + : state.value, + isTouched: state.meta.isTouched, + isBlurred: state.meta.isBlurred, + isDirty: state.meta.isDirty, + errorMap: state.meta.errorMap, + errorSourceMap: state.meta.errorSourceMap, + isValidating: state.meta.isValidating, + } + }, + (a, b) => + a.value === b.value && + a.isTouched === b.isTouched && + a.isBlurred === b.isBlurred && + a.isDirty === b.isDirty && + a.errorMap === b.errorMap && + a.errorSourceMap === b.errorSourceMap && + a.isValidating === b.isValidating, ) // This makes me sad, but if I understand correctly, this is what we have to do for reactivity to work properly with React compiler. @@ -262,19 +257,19 @@ export function useField< ...fieldApi, get state() { return { - // For array mode, reactiveStateValue is the length (for reactivity tracking), + // For array mode, reactiveState.value is the length (for reactivity tracking), // so we need to get the actual value from fieldApi value: - opts.mode === 'array' ? fieldApi.state.value : reactiveStateValue, + isArrayMode ? fieldApi.state.value : reactiveState.value, get meta() { return { ...fieldApi.state.meta, - isTouched: reactiveMetaIsTouched, - isBlurred: reactiveMetaIsBlurred, - isDirty: reactiveMetaIsDirty, - errorMap: reactiveMetaErrorMap, - errorSourceMap: reactiveMetaErrorSourceMap, - isValidating: reactiveMetaIsValidating, + isTouched: reactiveState.isTouched, + isBlurred: reactiveState.isBlurred, + isDirty: reactiveState.isDirty, + errorMap: reactiveState.errorMap, + errorSourceMap: reactiveState.errorSourceMap, + isValidating: reactiveState.isValidating, } satisfies AnyFieldMeta }, } satisfies AnyFieldApi['state'] @@ -326,14 +321,8 @@ export function useField< return extendedApi }, [ fieldApi, - opts.mode, - reactiveStateValue, - reactiveMetaIsTouched, - reactiveMetaIsBlurred, - reactiveMetaIsDirty, - reactiveMetaErrorMap, - reactiveMetaErrorSourceMap, - reactiveMetaIsValidating, + isArrayMode, + reactiveState, ]) useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi]) From d744da01c7c5930b9b9e86d4b23059151c849986 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 18:56:23 -0800 Subject: [PATCH 09/31] chore: fix benchmark tests --- packages/form-core/benchmark.ts | 245 ++++++++--------------------- packages/form-core/src/FieldApi.ts | 2 +- packages/form-core/src/FormApi.ts | 153 +++++++++++++----- 3 files changed, 181 insertions(+), 219 deletions(-) diff --git a/packages/form-core/benchmark.ts b/packages/form-core/benchmark.ts index ff08dacb1..4068bcbd7 100644 --- a/packages/form-core/benchmark.ts +++ b/packages/form-core/benchmark.ts @@ -1,10 +1,5 @@ /** - * Benchmark: Measures how FormApi + FieldApi perform under load - * - * Scenarios: - * 1. Many fields, single field change: Create N fields, change one field, measure recomputations - * 2. Many fields, sequential field changes: Create N fields, change each sequentially - * 3. Subscription count: How many subscribers fire when a single field changes + * Comprehensive benchmark: Measures full setValue + onChange validation performance */ import { FormApi } from './src/FormApi' @@ -13,30 +8,26 @@ import { FieldApi } from './src/FieldApi' const FIELD_COUNTS = [10, 50, 100, 500] const ITERATIONS = 100 -interface BenchResult { - scenario: string - fieldCount: number - totalMs: number - avgMs: number - subscriberCallCount: number -} - -function createFormWithFields(fieldCount: number) { +function createFormWithValidatedFields(fieldCount: number) { const defaultValues: Record = {} for (let i = 0; i < fieldCount; i++) { defaultValues[`field_${i}`] = `value_${i}` } - const form = new FormApi({ - defaultValues, - }) - - const fields: FieldApi[] = [] + const form = new FormApi({ defaultValues }) + const fields: any[] = [] for (let i = 0; i < fieldCount; i++) { const field = new FieldApi({ form, name: `field_${i}`, + validators: { + onChange: ({ value }: any) => { + if (!value) return 'Required' + if (value.length < 3) return 'Too short' + return undefined + }, + }, } as any) field.mount() fields.push(field) @@ -45,179 +36,69 @@ function createFormWithFields(fieldCount: number) { return { form, fields } } -// Scenario 1: Single field change with N fields registered -function benchSingleFieldChange(fieldCount: number): BenchResult { - const { form, fields } = createFormWithFields(fieldCount) - - let subscriberCallCount = 0 - // Subscribe to all fields to mimic real UI - const unsubs = fields.map((field) => - field.store.subscribe(() => { - subscriberCallCount++ - }), - ) - - // Warmup - for (let i = 0; i < 5; i++) { - fields[0]!.setValue(`warmup_${i}`) - } - subscriberCallCount = 0 - - const start = performance.now() - for (let i = 0; i < ITERATIONS; i++) { - fields[0]!.setValue(`new_value_${i}`) - } - const totalMs = performance.now() - start - - unsubs.forEach((u) => u.unsubscribe()) - - return { - scenario: 'single_field_change', - fieldCount, - totalMs, - avgMs: totalMs / ITERATIONS, - subscriberCallCount, - } -} - -// Scenario 2: Change each field once sequentially -function benchSequentialFieldChanges(fieldCount: number): BenchResult { - const { form, fields } = createFormWithFields(fieldCount) - - let subscriberCallCount = 0 - const unsubs = fields.map((field) => - field.store.subscribe(() => { - subscriberCallCount++ - }), - ) - - // Warmup - fields[0]!.setValue('warmup') - subscriberCallCount = 0 - - const start = performance.now() +function createFormNoValidation(fieldCount: number) { + const defaultValues: Record = {} for (let i = 0; i < fieldCount; i++) { - fields[i]!.setValue(`updated_${i}`) - } - const totalMs = performance.now() - start - - unsubs.forEach((u) => u.unsubscribe()) - - return { - scenario: 'sequential_field_changes', - fieldCount, - totalMs, - avgMs: totalMs / fieldCount, - subscriberCallCount, + defaultValues[`field_${i}`] = `value_${i}` } -} - -// Scenario 3: Measure store recomputations per single change -function benchStoreRecomputations(fieldCount: number): BenchResult { - const { form, fields } = createFormWithFields(fieldCount) - - let subscriberCallCount = 0 - const unsubs = fields.map((field) => - field.store.subscribe(() => { - subscriberCallCount++ - }), - ) - - // Single field change - measure how many subscribers fire - subscriberCallCount = 0 - fields[0]!.setValue('trigger_change') - const fireCount = subscriberCallCount + const form = new FormApi({ defaultValues }) + const fields: any[] = [] - unsubs.forEach((u) => u.unsubscribe()) - - return { - scenario: 'store_recomputations_per_change', - fieldCount, - totalMs: 0, - avgMs: 0, - subscriberCallCount: fireCount, + for (let i = 0; i < fieldCount; i++) { + const field = new FieldApi({ + form, + name: `field_${i}`, + } as any) + field.mount() + fields.push(field) } -} - -// Scenario 4: Form store subscription count per field change -function benchFormStoreSubscriptions(fieldCount: number): BenchResult { - const { form, fields } = createFormWithFields(fieldCount) - - let formStoreCallCount = 0 - const formUnsub = form.store.subscribe(() => { - formStoreCallCount++ - }) - - // Single field change - formStoreCallCount = 0 - fields[0]!.setValue('trigger_change') - formUnsub.unsubscribe() - - return { - scenario: 'form_store_subs_per_change', - fieldCount, - totalMs: 0, - avgMs: 0, - subscriberCallCount: formStoreCallCount, - } + return { form, fields } } -// Run all benchmarks console.log('=== TanStack Form Performance Benchmark ===\n') - -const results: BenchResult[] = [] +console.log('Fields | No Validation (ms/op) | With onChange (ms/op) | Sub calls/change') for (const count of FIELD_COUNTS) { - console.log(`--- ${count} fields ---`) - - const r1 = benchSingleFieldChange(count) - console.log( - ` Single field change (${ITERATIONS}x): ${r1.totalMs.toFixed(2)}ms total, ${r1.avgMs.toFixed(3)}ms avg, ${r1.subscriberCallCount} subscriber calls`, - ) - results.push(r1) - - const r2 = benchSequentialFieldChanges(count) - console.log( - ` Sequential changes: ${r2.totalMs.toFixed(2)}ms total, ${r2.avgMs.toFixed(3)}ms avg per field, ${r2.subscriberCallCount} subscriber calls`, - ) - results.push(r2) - - const r3 = benchStoreRecomputations(count) - console.log( - ` Subscriber fires per single change: ${r3.subscriberCallCount} (of ${count} fields)`, - ) - results.push(r3) - - const r4 = benchFormStoreSubscriptions(count) - console.log( - ` Form store fires per field change: ${r4.subscriberCallCount}`, - ) - results.push(r4) - - console.log() -} + // No validation + const { fields: nvFields } = createFormNoValidation(count) + let nvSubs = 0 + const nvUnsubs = nvFields.map((f: any) => f.store.subscribe(() => { nvSubs++ })) + nvSubs = 0 + const nvStart = performance.now() + for (let i = 0; i < ITERATIONS; i++) { + nvFields[0]!.setValue(`nv_${i}`) + } + const nvMs = (performance.now() - nvStart) / ITERATIONS + nvUnsubs.forEach((u: any) => u.unsubscribe()) + + // With onChange validation + const { fields: vFields } = createFormWithValidatedFields(count) + let vSubs = 0 + const vUnsubs = vFields.map((f: any) => f.store.subscribe(() => { vSubs++ })) + + // Count sub calls for one change + vSubs = 0 + vFields[0]!.setValue('x') + const subsPerChange = vSubs + let other = 0 + const otherUnsubs = vFields.slice(1).map((f: any) => f.store.subscribe(() => { other++ })) + other = 0 + vFields[0]!.setValue('y') + otherUnsubs.forEach((u: any) => u.unsubscribe()) + + // Timed run + vSubs = 0 + const vStart = performance.now() + for (let i = 0; i < ITERATIONS; i++) { + vFields[0]!.setValue(`v_${i}`) + } + const vMs = (performance.now() - vStart) / ITERATIONS + vUnsubs.forEach((u: any) => u.unsubscribe()) -// Summary table -console.log('\n=== Summary: Subscriber calls per single field change ===') -console.log('Fields | Subscribers Fired | Expected (ideal)') -for (const count of FIELD_COUNTS) { - const r = results.find( - (r) => - r.scenario === 'store_recomputations_per_change' && - r.fieldCount === count, - ) - // Ideal: only 1 subscriber should fire (the changed field) - console.log(`${count.toString().padStart(6)} | ${r!.subscriberCallCount.toString().padStart(17)} | 1`) + console.log(`${count.toString().padStart(6)} | ${nvMs.toFixed(3).padStart(21)} | ${vMs.toFixed(3).padStart(20)} | ${subsPerChange} total, ${other} other-field`) } -console.log('\n=== Summary: Time for single field change (ms avg) ===') -console.log('Fields | Avg time (ms)') -for (const count of FIELD_COUNTS) { - const r = results.find( - (r) => - r.scenario === 'single_field_change' && r.fieldCount === count, - ) - console.log(`${count.toString().padStart(6)} | ${r!.avgMs.toFixed(3)}`) -} +console.log('\n(BEFORE this refactor: 500 fields with onChange was ~0.615ms/op)') +console.log('(BEFORE validation opt: 500 fields with onChange was ~0.608ms/op, no-val was ~0.667ms/op)') diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 2ccefcc8c..627ea580d 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1424,7 +1424,7 @@ export class FieldApi< } } - if (!this.form.getFieldMeta(this.name)) { + if (!this.form._getFieldMetaBase(this.name)) { this.form.setFieldMeta(this.name, this.state.meta) } } diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 089a33cc3..ce16745d9 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1373,16 +1373,21 @@ export class FormApi< * Parent fields are affected because immutable updates create new references up the tree. * Child fields are affected because changing a parent value changes descendant values. */ - _notifyRelatedPerFieldValueStores = (field: string): void => { + _notifyRelatedPerFieldValueStores = ( + field: string, + skipSelf?: boolean, + ): void => { const newValues = this.baseStore.state.values - // Update the specific field - const store = this._perFieldStores[field] - if (store) { - store.setState((prev) => ({ - ...prev, - value: getBy(newValues, field), - })) + // Update the specific field (skip if caller already handled it) + if (!skipSelf) { + const store = this._perFieldStores[field] + if (store) { + store.setState((prev) => ({ + ...prev, + value: getBy(newValues, field), + })) + } } // Update parent paths (e.g., for 'items[0].name', update 'items' and 'items[0]') @@ -1795,6 +1800,9 @@ export class FormApi< ...Object.keys(fieldErrors || {}), ] as DeepKeys[]) + // Collect all field meta changes to batch into a single baseStore.setState + const pendingMetaChanges: Record = {} + for (const field of allFieldsToProcess) { if ( this.baseStore.state.fieldMetaBase[field] === undefined && @@ -1832,17 +1840,39 @@ export class FormApi< // 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, + pendingMetaChanges[field as string] = { + ...fieldMetaBase, errorMap: { - ...prev.errorMap, + ...fieldMetaBase.errorMap, [errorMapKey]: newErrorValue, }, errorSourceMap: { - ...prev.errorSourceMap, + ...fieldMetaBase.errorSourceMap, [errorMapKey]: newSource, }, - })) + } + } + } + + // Apply all field meta changes in a single baseStore.setState + if (Object.keys(pendingMetaChanges).length > 0) { + this.baseStore.setState((prev) => ({ + ...prev, + fieldMetaBase: { + ...prev.fieldMetaBase, + ...pendingMetaChanges, + }, + })) + + // Update per-field stores for affected fields + for (const [field, meta] of Object.entries(pendingMetaChanges)) { + const perFieldStore = this._perFieldStores[field] + if (perFieldStore) { + perFieldStore.setState((prev) => ({ + ...prev, + metaBase: meta, + })) + } } } @@ -1945,6 +1975,8 @@ export class FormApi< | Partial, ValidationError>> | undefined + const pendingAsyncMetaChanges: Record = {} + for (const validateObj of validates) { if (!validateObj.validate) continue const key = getErrorMapKey(validateObj.cause) @@ -2032,17 +2064,40 @@ export class FormApi< // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition currentErrorMap?.[errorMapKey] !== newErrorValue ) { - this.setFieldMeta(field, (prev) => ({ - ...prev, + pendingAsyncMetaChanges[field as string] = { + ...fieldMetaBase, errorMap: { - ...prev.errorMap, + ...fieldMetaBase.errorMap, [errorMapKey]: newErrorValue, }, errorSourceMap: { - ...prev.errorSourceMap, + ...fieldMetaBase.errorSourceMap, [errorMapKey]: newSource, }, - })) + } + } + } + + // Apply all field meta changes in a single baseStore.setState + if (Object.keys(pendingAsyncMetaChanges).length > 0) { + this.baseStore.setState((prev) => ({ + ...prev, + fieldMetaBase: { + ...prev.fieldMetaBase, + ...pendingAsyncMetaChanges, + }, + })) + + for (const [af, ameta] of Object.entries( + pendingAsyncMetaChanges, + )) { + const perFieldStore = this._perFieldStores[af] + if (perFieldStore) { + perFieldStore.setState((prev) => ({ + ...prev, + metaBase: ameta, + })) + } } } @@ -2411,30 +2466,56 @@ 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, - }, - })) - } - + // Combine meta + value update into a single baseStore.setState to avoid + // multiple derived store recomputations and signal propagation cycles + let newFieldMeta: AnyFieldMetaBase | undefined this.baseStore.setState((prev) => { + const newValues = setBy(prev.values, field, updater) + if (!dontUpdateMeta) { + const prevMeta = prev.fieldMetaBase[field] as AnyFieldMetaBase | undefined + newFieldMeta = { + ...prevMeta, + isTouched: true, + isDirty: true, + errorMap: { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + ...prevMeta?.errorMap, + onMount: undefined, + }, + } as AnyFieldMetaBase + return { + ...prev, + values: newValues, + fieldMetaBase: { + ...prev.fieldMetaBase, + [field]: newFieldMeta, + }, + } + } return { ...prev, - values: setBy(prev.values, field, updater), + values: newValues, } }) - // Notify per-field value stores for this field and related parent/child fields - this._notifyRelatedPerFieldValueStores(field as string) - // }) + // Notify per-field stores: combine meta + value in a single setState when possible + const perFieldStore = this._perFieldStores[field as string] + if (perFieldStore) { + const newValue = getBy(this.baseStore.state.values, field as string) + if (newFieldMeta) { + perFieldStore.setState(() => ({ + value: newValue, + metaBase: newFieldMeta!, + })) + } else { + perFieldStore.setState((prev) => ({ + ...prev, + value: newValue, + })) + } + } + // Still notify parent/child per-field stores for value changes + this._notifyRelatedPerFieldValueStores(field as string, true) if (!dontRunListeners) { this.getFieldInfo(field).instance?.triggerOnChangeListener() From abc7b91d38a40e764c4eb96c7d9b0468d7be75f1 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 19:02:43 -0800 Subject: [PATCH 10/31] chore: remove fieldMetaDerived --- packages/form-core/src/FormApi.ts | 232 ++++++++++++------------------ 1 file changed, 94 insertions(+), 138 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index ce16745d9..217bbe992 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -891,21 +891,6 @@ export class FormApi< TOnServer > > - fieldMetaDerived: Store< - FormState< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer - >['fieldMeta'] - > store: Store< FormState< TFormData, @@ -1046,7 +1031,7 @@ export class FormApi< this.baseStore = createStore(baseStoreVal) as never - let prevBaseStore: + let prevBaseStoreForStore: | BaseFormState< TFormData, TOnMount, @@ -1062,144 +1047,115 @@ export class FormApi< > | undefined = undefined - this.fieldMetaDerived = createStore( - (prevVal: Record, AnyFieldMeta> | undefined) => { - const currBaseStore = this.baseStore.get() - - let originalMetaCount = 0 - - const fieldMeta: FormState< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer - >['fieldMeta'] = {} + this.store = createStore< + FormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + > + >((prevVal) => { + const currBaseStore = this.baseStore.get() - for (const fieldName of Object.keys( - currBaseStore.fieldMetaBase, - ) as Array) { - const currBaseMeta = currBaseStore.fieldMetaBase[ - fieldName as never - ] as AnyFieldMetaBase + // Derive fieldMeta from baseStore.fieldMetaBase (inlined from former fieldMetaDerived) + let originalMetaCount = 0 + const prevFieldMetaObj = prevVal?.fieldMeta - const prevBaseMeta = prevBaseStore?.fieldMetaBase[ - fieldName as never - ] as AnyFieldMetaBase | undefined + const currFieldMeta: FormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + >['fieldMeta'] = {} - const prevFieldInfo = - prevVal?.[fieldName as never as keyof typeof prevVal] + for (const fieldName of Object.keys( + currBaseStore.fieldMetaBase, + ) as Array) { + const currBaseMeta = currBaseStore.fieldMetaBase[ + fieldName as never + ] as AnyFieldMetaBase - const curFieldVal = getBy(currBaseStore.values, fieldName) + const prevBaseMeta = prevBaseStoreForStore?.fieldMetaBase[ + fieldName as never + ] as AnyFieldMetaBase | undefined - let fieldErrors = prevFieldInfo?.errors - if ( - !prevBaseMeta || - currBaseMeta.errorMap !== prevBaseMeta.errorMap - ) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - fieldErrors = Object.values(currBaseMeta.errorMap ?? {}).filter( - (val) => val !== undefined, - ) + const prevFieldInfo = + prevFieldMetaObj?.[fieldName as never as keyof typeof prevFieldMetaObj] - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const fieldInstance = this.getFieldInfo(fieldName)?.instance + const curFieldVal = getBy(currBaseStore.values, fieldName) - if (!fieldInstance || !fieldInstance.options.disableErrorFlat) { - fieldErrors = fieldErrors.flat(1) - } - } + let fieldErrors = prevFieldInfo?.errors + if ( + !prevBaseMeta || + currBaseMeta.errorMap !== prevBaseMeta.errorMap + ) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + fieldErrors = Object.values(currBaseMeta.errorMap ?? {}).filter( + (val) => val !== undefined, + ) - // As primitives, we don't need to aggressively persist the same referential value for performance reasons - const isFieldValid = !isNonEmptyArray(fieldErrors) - const isFieldPristine = !currBaseMeta.isDirty - const isDefaultValue = - evaluate( - curFieldVal, - getBy(this.options.defaultValues, fieldName), - ) || - evaluate( - curFieldVal, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.getFieldInfo(fieldName)?.instance?.options.defaultValue, - ) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const fieldInstance = this.getFieldInfo(fieldName)?.instance - if ( - prevFieldInfo && - prevFieldInfo.isPristine === isFieldPristine && - prevFieldInfo.isValid === isFieldValid && - prevFieldInfo.isDefaultValue === isDefaultValue && - prevFieldInfo.errors === fieldErrors && - currBaseMeta === prevBaseMeta - ) { - fieldMeta[fieldName] = prevFieldInfo - originalMetaCount++ - continue + if (!fieldInstance || !fieldInstance.options.disableErrorFlat) { + fieldErrors = fieldErrors.flat(1) } - - fieldMeta[fieldName] = { - ...currBaseMeta, - errors: fieldErrors ?? [], - isPristine: isFieldPristine, - isValid: isFieldValid, - isDefaultValue: isDefaultValue, - } satisfies AnyFieldMeta as AnyFieldMeta } - if (!Object.keys(currBaseStore.fieldMetaBase).length) return fieldMeta + const isFieldValid = !isNonEmptyArray(fieldErrors) + const isFieldPristine = !currBaseMeta.isDirty + const isDefaultValue = + evaluate( + curFieldVal, + getBy(this.options.defaultValues, fieldName), + ) || + evaluate( + curFieldVal, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.getFieldInfo(fieldName)?.instance?.options.defaultValue, + ) if ( - prevVal && - originalMetaCount === Object.keys(currBaseStore.fieldMetaBase).length + prevFieldInfo && + prevFieldInfo.isPristine === isFieldPristine && + prevFieldInfo.isValid === isFieldValid && + prevFieldInfo.isDefaultValue === isDefaultValue && + prevFieldInfo.errors === fieldErrors && + currBaseMeta === prevBaseMeta ) { - return prevVal + currFieldMeta[fieldName] = prevFieldInfo + originalMetaCount++ + continue } - prevBaseStore = this.baseStore.get() - - return fieldMeta - }, - ) as never - - let prevBaseStoreForStore: - | BaseFormState< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer - > - | undefined = undefined + currFieldMeta[fieldName] = { + ...currBaseMeta, + errors: fieldErrors ?? [], + isPristine: isFieldPristine, + isValid: isFieldValid, + isDefaultValue: isDefaultValue, + } satisfies AnyFieldMeta as AnyFieldMeta + } - 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() + const fieldMetaBaseKeyCount = Object.keys(currBaseStore.fieldMetaBase).length + const fieldMetaUnchanged = + fieldMetaBaseKeyCount > 0 && + prevFieldMetaObj && + originalMetaCount === fieldMetaBaseKeyCount // Computed state const fieldMetaValues = Object.values(currFieldMeta).filter( @@ -1283,7 +1239,7 @@ export class FormApi< prevVal && prevBaseStoreForStore && prevVal.errorMap === errorMap && - prevVal.fieldMeta === this.fieldMetaDerived.state && + fieldMetaUnchanged && prevVal.errors === errors && prevVal.isFieldsValidating === isFieldsValidating && prevVal.isFieldsValid === isFieldsValid && @@ -1303,7 +1259,7 @@ export class FormApi< const state = { ...currBaseStore, errorMap, - fieldMeta: this.fieldMetaDerived.state, + fieldMeta: fieldMetaUnchanged ? prevFieldMetaObj! : currFieldMeta, errors, isFieldsValidating, isFieldsValid, From ce2a88353cd8188ca34ff1d5fc85d24083917eca Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 19:07:02 -0800 Subject: [PATCH 11/31] chore: refactor work on parent fields --- packages/form-core/src/FormApi.ts | 40 ++++++++++++------------------- packages/form-core/src/utils.ts | 18 ++++++++++++++ 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 217bbe992..98f4e19f0 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -6,6 +6,7 @@ import { functionalUpdate, getAsyncValidatorArray, getBy, + getParentPaths, getSyncValidatorArray, isGlobalFormValidationError, isNonEmptyArray, @@ -1347,19 +1348,14 @@ export class FormApi< } // Update parent paths (e.g., for 'items[0].name', update 'items' and 'items[0]') - let parentPath = '' - for (let i = 0; i < field.length; i++) { - const char = field[i] - if (char === '.' || char === '[') { - const parentStore = this._perFieldStores[parentPath] - if (parentStore) { - parentStore.setState((prev) => ({ - ...prev, - value: getBy(newValues, parentPath), - })) - } + for (const parentPath of getParentPaths(field)) { + const parentStore = this._perFieldStores[parentPath] + if (parentStore) { + parentStore.setState((prev) => ({ + ...prev, + value: getBy(newValues, parentPath), + })) } - parentPath += char } // Update child paths (e.g., for 'items', update 'items[0]', 'items[0].name', etc.) @@ -2519,20 +2515,14 @@ export class FormApi< // Notify parent per-field stores that their values may have changed const newValues = this.baseStore.state.values for (const f of fieldsToDelete) { - const fieldStr = f as string - let parentPath = '' - for (let i = 0; i < fieldStr.length; i++) { - const char = fieldStr[i] - if (char === '.' || char === '[') { - const parentStore = this._perFieldStores[parentPath] - if (parentStore) { - parentStore.setState((prev) => ({ - ...prev, - value: getBy(newValues, parentPath), - })) - } + for (const parentPath of getParentPaths(f as string)) { + const parentStore = this._perFieldStores[parentPath] + if (parentStore) { + parentStore.setState((prev) => ({ + ...prev, + value: getBy(newValues, parentPath), + })) } - parentPath += char } } } diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index 71a6ddb69..7ee12fbdd 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -89,6 +89,24 @@ export function setBy(obj: any, _path: any, updater: Updater) { return doSet(obj) } +/** + * Extract all parent paths from a field path string. + * E.g., for 'items[0].name' returns ['items', 'items[0]']. + * @private + */ +export function getParentPaths(field: string): string[] { + const parents: string[] = [] + let parentPath = '' + for (let i = 0; i < field.length; i++) { + const char = field[i] + if (char === '.' || char === '[') { + parents.push(parentPath) + } + parentPath += char + } + return parents +} + /** * Delete a field on an object using a path, including dot notation. * @private From 1d8a26659448f77758d2c88ffba6b6fd08176f03 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 19:10:33 -0800 Subject: [PATCH 12/31] chore: refactor `_notifyRelatedPerFieldValueStores` --- packages/form-core/src/FormApi.ts | 38 ++++++++++--------------------- packages/form-core/src/utils.ts | 17 ++++++++++++++ 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 98f4e19f0..f21dfe2f2 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -6,6 +6,7 @@ import { functionalUpdate, getAsyncValidatorArray, getBy, + getChildPaths, getParentPaths, getSyncValidatorArray, isGlobalFormValidationError, @@ -1326,48 +1327,33 @@ export class FormApi< /** * @private - * Notifies per-field value stores for the changed field AND any related parent/child fields. + * Notifies parent and child per-field value stores when a field's value changes. * Parent fields are affected because immutable updates create new references up the tree. * Child fields are affected because changing a parent value changes descendant values. + * Does NOT update the field itself — the caller is expected to handle that + * (often combined with a meta update in a single setState for performance). */ - _notifyRelatedPerFieldValueStores = ( - field: string, - skipSelf?: boolean, - ): void => { + _notifyRelatedPerFieldValueStores = (field: string): void => { const newValues = this.baseStore.state.values - // Update the specific field (skip if caller already handled it) - if (!skipSelf) { - const store = this._perFieldStores[field] + const updateStoreValue = (path: string) => { + const store = this._perFieldStores[path] if (store) { store.setState((prev) => ({ ...prev, - value: getBy(newValues, field), + value: getBy(newValues, path), })) } } // Update parent paths (e.g., for 'items[0].name', update 'items' and 'items[0]') for (const parentPath of getParentPaths(field)) { - const parentStore = this._perFieldStores[parentPath] - if (parentStore) { - parentStore.setState((prev) => ({ - ...prev, - value: getBy(newValues, parentPath), - })) - } + updateStoreValue(parentPath) } // Update child paths (e.g., for 'items', update 'items[0]', 'items[0].name', etc.) - const fieldDot = field + '.' - const fieldBracket = field + '[' - for (const key of Object.keys(this._perFieldStores)) { - if (key.startsWith(fieldDot) || key.startsWith(fieldBracket)) { - this._perFieldStores[key]!.setState((prev) => ({ - ...prev, - value: getBy(newValues, key), - })) - } + for (const childPath of getChildPaths(field, Object.keys(this._perFieldStores))) { + updateStoreValue(childPath) } } @@ -2467,7 +2453,7 @@ export class FormApi< } } // Still notify parent/child per-field stores for value changes - this._notifyRelatedPerFieldValueStores(field as string, true) + this._notifyRelatedPerFieldValueStores(field as string) if (!dontRunListeners) { this.getFieldInfo(field).instance?.triggerOnChangeListener() diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index 7ee12fbdd..03108ca04 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -107,6 +107,23 @@ export function getParentPaths(field: string): string[] { return parents } +/** + * Check whether `key` is a direct child path of `field`. + * E.g., for field 'items', keys 'items[0]' and 'items.foo' are children. + * @private + */ +export function isChildPath(field: string, key: string): boolean { + return key.startsWith(field + '.') || key.startsWith(field + '[') +} + +/** + * Return all keys from `allKeys` that are child paths of `field`. + * @private + */ +export function getChildPaths(field: string, allKeys: string[]): string[] { + return allKeys.filter((key) => isChildPath(field, key)) +} + /** * Delete a field on an object using a path, including dot notation. * @private From 8ca5fdbe93c95732c7abb96d4dbb2518a0d52010 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 19:18:07 -0800 Subject: [PATCH 13/31] chore: remove interim benchmark --- packages/form-core/benchmark.ts | 104 -------------------------------- 1 file changed, 104 deletions(-) delete mode 100644 packages/form-core/benchmark.ts diff --git a/packages/form-core/benchmark.ts b/packages/form-core/benchmark.ts deleted file mode 100644 index 4068bcbd7..000000000 --- a/packages/form-core/benchmark.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Comprehensive benchmark: Measures full setValue + onChange validation performance - */ - -import { FormApi } from './src/FormApi' -import { FieldApi } from './src/FieldApi' - -const FIELD_COUNTS = [10, 50, 100, 500] -const ITERATIONS = 100 - -function createFormWithValidatedFields(fieldCount: number) { - const defaultValues: Record = {} - for (let i = 0; i < fieldCount; i++) { - defaultValues[`field_${i}`] = `value_${i}` - } - - const form = new FormApi({ defaultValues }) - const fields: any[] = [] - - for (let i = 0; i < fieldCount; i++) { - const field = new FieldApi({ - form, - name: `field_${i}`, - validators: { - onChange: ({ value }: any) => { - if (!value) return 'Required' - if (value.length < 3) return 'Too short' - return undefined - }, - }, - } as any) - field.mount() - fields.push(field) - } - - return { form, fields } -} - -function createFormNoValidation(fieldCount: number) { - const defaultValues: Record = {} - for (let i = 0; i < fieldCount; i++) { - defaultValues[`field_${i}`] = `value_${i}` - } - - const form = new FormApi({ defaultValues }) - const fields: any[] = [] - - for (let i = 0; i < fieldCount; i++) { - const field = new FieldApi({ - form, - name: `field_${i}`, - } as any) - field.mount() - fields.push(field) - } - - return { form, fields } -} - -console.log('=== TanStack Form Performance Benchmark ===\n') -console.log('Fields | No Validation (ms/op) | With onChange (ms/op) | Sub calls/change') - -for (const count of FIELD_COUNTS) { - // No validation - const { fields: nvFields } = createFormNoValidation(count) - let nvSubs = 0 - const nvUnsubs = nvFields.map((f: any) => f.store.subscribe(() => { nvSubs++ })) - nvSubs = 0 - const nvStart = performance.now() - for (let i = 0; i < ITERATIONS; i++) { - nvFields[0]!.setValue(`nv_${i}`) - } - const nvMs = (performance.now() - nvStart) / ITERATIONS - nvUnsubs.forEach((u: any) => u.unsubscribe()) - - // With onChange validation - const { fields: vFields } = createFormWithValidatedFields(count) - let vSubs = 0 - const vUnsubs = vFields.map((f: any) => f.store.subscribe(() => { vSubs++ })) - - // Count sub calls for one change - vSubs = 0 - vFields[0]!.setValue('x') - const subsPerChange = vSubs - let other = 0 - const otherUnsubs = vFields.slice(1).map((f: any) => f.store.subscribe(() => { other++ })) - other = 0 - vFields[0]!.setValue('y') - otherUnsubs.forEach((u: any) => u.unsubscribe()) - - // Timed run - vSubs = 0 - const vStart = performance.now() - for (let i = 0; i < ITERATIONS; i++) { - vFields[0]!.setValue(`v_${i}`) - } - const vMs = (performance.now() - vStart) / ITERATIONS - vUnsubs.forEach((u: any) => u.unsubscribe()) - - console.log(`${count.toString().padStart(6)} | ${nvMs.toFixed(3).padStart(21)} | ${vMs.toFixed(3).padStart(20)} | ${subsPerChange} total, ${other} other-field`) -} - -console.log('\n(BEFORE this refactor: 500 fields with onChange was ~0.615ms/op)') -console.log('(BEFORE validation opt: 500 fields with onChange was ~0.608ms/op, no-val was ~0.667ms/op)') From 2ee7fca7d7e06b8a08cd5985fc90704704f3e30c Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 19:21:53 -0800 Subject: [PATCH 14/31] chore: remove any typings --- packages/form-core/src/FieldApi.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 627ea580d..49c3b7d7a 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1168,9 +1168,9 @@ export class FieldApi< formListeners: {} as Record, } - let prevMetaBase: any = undefined - let prevRawValue: any = undefined - let cachedMeta: any = undefined + let prevMetaBase: AnyFieldMetaBase | undefined = undefined + let prevRawValue: unknown = undefined + let cachedMeta: AnyFieldMeta | undefined = undefined this.store = createStore( ( @@ -1208,7 +1208,7 @@ export class FieldApi< const { value: rawValue, metaBase } = perFieldStore.get() // Compute derived meta with caching - let meta: any + let meta: AnyFieldMeta if (metaBase === prevMetaBase && rawValue === prevRawValue && cachedMeta) { // Nothing changed, reuse cached meta meta = cachedMeta From 52962deb00e2520111d61cd18d2e67e00737fe5a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:18:57 +0000 Subject: [PATCH 15/31] ci: apply automated fixes and generate docs --- packages/form-core/src/FieldApi.ts | 9 +++-- packages/form-core/src/FormApi.ts | 55 ++++++++++++++-------------- packages/react-form/src/useField.tsx | 9 +---- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 49c3b7d7a..ac7062c64 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1209,7 +1209,11 @@ export class FieldApi< // Compute derived meta with caching let meta: AnyFieldMeta - if (metaBase === prevMetaBase && rawValue === prevRawValue && cachedMeta) { + if ( + metaBase === prevMetaBase && + rawValue === prevRawValue && + cachedMeta + ) { // Nothing changed, reuse cached meta meta = cachedMeta } else { @@ -1231,8 +1235,7 @@ export class FieldApi< evaluate( rawValue, getBy(this.form.options.defaultValues, this.name), - ) || - evaluate(rawValue, this.options.defaultValue) + ) || evaluate(rawValue, this.options.defaultValue) // Check if derived values actually changed - preserve reference if not if ( diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index f21dfe2f2..619c4afd8 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -918,8 +918,10 @@ export class FormApi< * Per-field mutable stores that hold { value, metaBase } for each registered field. * FieldApi instances subscribe to these instead of the full form store for O(1) reactivity. */ - _perFieldStores: Record> = - {} as any + _perFieldStores: Record< + string, + Store<{ value: any; metaBase: AnyFieldMetaBase }> + > = {} as any get state() { return this.store.state @@ -1084,9 +1086,9 @@ export class FormApi< TOnServer >['fieldMeta'] = {} - for (const fieldName of Object.keys( - currBaseStore.fieldMetaBase, - ) as Array) { + for (const fieldName of Object.keys(currBaseStore.fieldMetaBase) as Array< + keyof typeof currBaseStore.fieldMetaBase + >) { const currBaseMeta = currBaseStore.fieldMetaBase[ fieldName as never ] as AnyFieldMetaBase @@ -1096,15 +1098,14 @@ export class FormApi< ] as AnyFieldMetaBase | undefined const prevFieldInfo = - prevFieldMetaObj?.[fieldName as never as keyof typeof prevFieldMetaObj] + prevFieldMetaObj?.[ + fieldName as never as keyof typeof prevFieldMetaObj + ] const curFieldVal = getBy(currBaseStore.values, fieldName) let fieldErrors = prevFieldInfo?.errors - if ( - !prevBaseMeta || - currBaseMeta.errorMap !== prevBaseMeta.errorMap - ) { + if (!prevBaseMeta || currBaseMeta.errorMap !== prevBaseMeta.errorMap) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition fieldErrors = Object.values(currBaseMeta.errorMap ?? {}).filter( (val) => val !== undefined, @@ -1121,10 +1122,7 @@ export class FormApi< const isFieldValid = !isNonEmptyArray(fieldErrors) const isFieldPristine = !currBaseMeta.isDirty const isDefaultValue = - evaluate( - curFieldVal, - getBy(this.options.defaultValues, fieldName), - ) || + evaluate(curFieldVal, getBy(this.options.defaultValues, fieldName)) || evaluate( curFieldVal, // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -1153,7 +1151,9 @@ export class FormApi< } satisfies AnyFieldMeta as AnyFieldMeta } - const fieldMetaBaseKeyCount = Object.keys(currBaseStore.fieldMetaBase).length + const fieldMetaBaseKeyCount = Object.keys( + currBaseStore.fieldMetaBase, + ).length const fieldMetaUnchanged = fieldMetaBaseKeyCount > 0 && prevFieldMetaObj && @@ -1352,7 +1352,10 @@ export class FormApi< } // Update child paths (e.g., for 'items', update 'items[0]', 'items[0].name', etc.) - for (const childPath of getChildPaths(field, Object.keys(this._perFieldStores))) { + for (const childPath of getChildPaths( + field, + Object.keys(this._perFieldStores), + )) { updateStoreValue(childPath) } } @@ -1367,10 +1370,9 @@ export class FormApi< for (const field of Object.keys(this._perFieldStores)) { this._perFieldStores[field]!.setState(() => ({ value: getBy(state.values, field), - metaBase: - (state.fieldMetaBase[ - field as keyof typeof state.fieldMetaBase - ] as AnyFieldMetaBase) ?? { ...defaultFieldMeta }, + metaBase: (state.fieldMetaBase[ + field as keyof typeof state.fieldMetaBase + ] as AnyFieldMetaBase) ?? { ...defaultFieldMeta }, })) } } @@ -2026,9 +2028,7 @@ export class FormApi< }, })) - for (const [af, ameta] of Object.entries( - pendingAsyncMetaChanges, - )) { + for (const [af, ameta] of Object.entries(pendingAsyncMetaChanges)) { const perFieldStore = this._perFieldStores[af] if (perFieldStore) { perFieldStore.setState((prev) => ({ @@ -2353,10 +2353,7 @@ export class FormApi< ) => { let newMeta: AnyFieldMetaBase this.baseStore.setState((prev) => { - newMeta = functionalUpdate( - updater, - prev.fieldMetaBase[field] as never, - ) + newMeta = functionalUpdate(updater, prev.fieldMetaBase[field] as never) return { ...prev, fieldMetaBase: { @@ -2410,7 +2407,9 @@ export class FormApi< this.baseStore.setState((prev) => { const newValues = setBy(prev.values, field, updater) if (!dontUpdateMeta) { - const prevMeta = prev.fieldMetaBase[field] as AnyFieldMetaBase | undefined + const prevMeta = prev.fieldMetaBase[field] as + | AnyFieldMetaBase + | undefined newFieldMeta = { ...prevMeta, isTouched: true, diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx index dd40a1603..bd7028677 100644 --- a/packages/react-form/src/useField.tsx +++ b/packages/react-form/src/useField.tsx @@ -259,8 +259,7 @@ export function useField< return { // For array mode, reactiveState.value is the length (for reactivity tracking), // so we need to get the actual value from fieldApi - value: - isArrayMode ? fieldApi.state.value : reactiveState.value, + value: isArrayMode ? fieldApi.state.value : reactiveState.value, get meta() { return { ...fieldApi.state.meta, @@ -319,11 +318,7 @@ export function useField< extendedApi.Field = Field as never return extendedApi - }, [ - fieldApi, - isArrayMode, - reactiveState, - ]) + }, [fieldApi, isArrayMode, reactiveState]) useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi]) From 0ab423162f477ac0017c02aa2db56d9df0dd9a16 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 19:26:27 -0800 Subject: [PATCH 16/31] chore: add onblur benchmark --- .../tests/onblur-detection.bench.tsx | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 packages/react-form/tests/onblur-detection.bench.tsx diff --git a/packages/react-form/tests/onblur-detection.bench.tsx b/packages/react-form/tests/onblur-detection.bench.tsx new file mode 100644 index 000000000..91e5718ce --- /dev/null +++ b/packages/react-form/tests/onblur-detection.bench.tsx @@ -0,0 +1,264 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +import { bench, describe } from 'vitest' + +import { z } from 'zod' +import { cleanup, fireEvent, render } from '@testing-library/react' +import { Formik, Field as FormikField } from 'formik' +import { toFormikValidationSchema } from 'zod-formik-adapter' +import { Controller, useForm as useReactHookForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { ErrorMessage } from '@hookform/error-message' +import { useForm as useTanStackForm } from '../src' +import type { FieldProps } from 'formik/dist/Field' + +const arr = Array.from({ length: 100 }, (_, i) => i) + +const validator = z.object({ + num: z.array(z.number().min(3, 'Must be at least three')), +}) + +function TanStackFormOnBlurBenchmark() { + const form = useTanStackForm({ + defaultValues: { num: arr }, + validators: { + onBlur: validator, + }, + }) + + return ( + <> + {arr.map((_num, i) => { + return ( + + {(field) => { + return ( +
+ field.handleChange(e.target.valueAsNumber)} + placeholder={`Number ${i}`} + /> + {field.state.meta.errors.map((error) => ( +

{error?.message}

+ ))} +
+ ) + }} +
+ ) + })} + + ) +} + +function FormikOnBlurBenchmark() { + return ( + {}} + > + {() => ( + <> + {arr.map((_num, i) => ( + + {(props: FieldProps) => ( +
+ + {props.meta.error} +
+ )} +
+ ))} + + )} +
+ ) +} + +function ReactHookFormOnBlurBenchmark() { + const { + register, + formState: { errors }, + } = useReactHookForm({ + defaultValues: { + num: arr, + }, + mode: 'onBlur', + resolver: zodResolver(validator), + }) + + return ( + <> + {arr.map((_num, i) => { + return ( +
+ + +
+ ) + })} + + ) +} + +function ReactHookFormHeadlessOnBlurBenchmark() { + const { control, handleSubmit } = useReactHookForm({ + defaultValues: { + num: arr, + }, + mode: 'onBlur', + resolver: zodResolver(validator), + }) + + return ( + <> + {arr.map((_num, i) => { + return ( + { + return ( +
+ onChange(event.target.valueAsNumber)} + placeholder={`Number ${i}`} + /> + {error &&

{error.message}

} +
+ ) + }} + name={`num.${i}`} + /> + ) + })} + + ) +} + +describe('Validates onBlur on 1,000 form items', () => { + bench( + 'TanStack Form', + async () => { + const { getByTestId, findAllByText, queryAllByText } = render( + , + ) + + if (queryAllByText('Must be at least three')?.length) { + throw 'Should not be present yet' + } + + fireEvent.blur(getByTestId('value1')) + + await findAllByText('Must be at least three') + }, + { + setup(task) { + task.opts.beforeEach = () => { + cleanup() + } + }, + }, + ) + + bench( + 'Formik', + async () => { + const { getByTestId, findAllByText, queryAllByText } = render( + , + ) + + if (queryAllByText('Must be at least three')?.length) { + throw 'Should not be present yet' + } + + fireEvent.blur(getByTestId('value1')) + + await findAllByText('Must be at least three') + }, + { + setup(task) { + task.opts.beforeEach = () => { + cleanup() + } + }, + }, + ) + + bench( + 'React Hook Form', + async () => { + const { getByTestId, findAllByText, queryAllByText } = render( + , + ) + + if (queryAllByText('Must be at least three')?.length) { + throw 'Should not be present yet' + } + + fireEvent.blur(getByTestId('value1')) + + await findAllByText('Must be at least three') + }, + { + setup(task) { + task.opts.beforeEach = () => { + cleanup() + } + }, + }, + ) + + bench( + 'React Hook Form (Headless)', + async () => { + const { getByTestId, findAllByText, queryAllByText } = render( + , + ) + + if (queryAllByText('Must be at least three')?.length) { + throw 'Should not be present yet' + } + + fireEvent.blur(getByTestId('value1')) + + await findAllByText('Must be at least three') + }, + { + setup(task) { + task.opts.beforeEach = () => { + cleanup() + } + }, + }, + ) +}) From 5ce4f67da824bce5bc4698b747bfebbab5b1ab4c Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 19:29:29 -0800 Subject: [PATCH 17/31] chore: add onMount validation --- .../tests/onmount-detection.bench.tsx | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 packages/react-form/tests/onmount-detection.bench.tsx diff --git a/packages/react-form/tests/onmount-detection.bench.tsx b/packages/react-form/tests/onmount-detection.bench.tsx new file mode 100644 index 000000000..4122f706a --- /dev/null +++ b/packages/react-form/tests/onmount-detection.bench.tsx @@ -0,0 +1,128 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +import { bench, describe } from 'vitest' + +import { z } from 'zod' +import { cleanup, fireEvent, render } from '@testing-library/react' +import { Formik, Field as FormikField } from 'formik' +import { toFormikValidationSchema } from 'zod-formik-adapter' +import { useForm as useTanStackForm } from '../src' +import type { FieldProps } from 'formik/dist/Field' + +const arr = Array.from({ length: 100 }, (_, i) => i) + +const validator = z.object({ + num: z.array(z.number().min(3, 'Must be at least three')), +}) + +function TanStackFormOnMountBenchmark() { + const form = useTanStackForm({ + defaultValues: { num: arr }, + validators: { + onMount: validator, + }, + }) + + return ( + <> + {arr.map((_num, i) => { + return ( + + {(field) => { + return ( +
+ field.handleChange(e.target.valueAsNumber)} + placeholder={`Number ${i}`} + /> + {field.state.meta.errors.map((error) => ( +

{error?.message}

+ ))} +
+ ) + }} +
+ ) + })} + + ) +} + +function FormikOnMountBenchmark() { + return ( + { }} + > + {() => ( + <> + {arr.map((_num, i) => ( + + {(props: FieldProps) => ( +
+ + {props.meta.error} +
+ )} +
+ ))} + + )} +
+ ) +} + +describe('Validates onMount on 1,000 form items', () => { + bench( + 'TanStack Form', + async () => { + const { findAllByText } = render( + , + ) + + await findAllByText('Must be at least three') + }, + { + setup(task) { + task.opts.beforeEach = () => { + cleanup() + } + }, + }, + ) + + bench( + 'Formik', + async () => { + const { findAllByText } = render( + , + ) + + await findAllByText('Must be at least three') + }, + { + setup(task) { + task.opts.beforeEach = () => { + cleanup() + } + }, + }, + ) +}) From 13f3909281a3f3573c9e11fab4e61c4f37bda5bb Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:30:24 +0000 Subject: [PATCH 18/31] ci: apply automated fixes and generate docs --- packages/react-form/tests/onmount-detection.bench.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/react-form/tests/onmount-detection.bench.tsx b/packages/react-form/tests/onmount-detection.bench.tsx index 4122f706a..d1113fb43 100644 --- a/packages/react-form/tests/onmount-detection.bench.tsx +++ b/packages/react-form/tests/onmount-detection.bench.tsx @@ -61,7 +61,7 @@ function FormikOnMountBenchmark() { num: arr, }} validationSchema={toFormikValidationSchema(validator)} - onSubmit={() => { }} + onSubmit={() => {}} > {() => ( <> @@ -93,9 +93,7 @@ describe('Validates onMount on 1,000 form items', () => { bench( 'TanStack Form', async () => { - const { findAllByText } = render( - , - ) + const { findAllByText } = render() await findAllByText('Must be at least three') }, @@ -111,9 +109,7 @@ describe('Validates onMount on 1,000 form items', () => { bench( 'Formik', async () => { - const { findAllByText } = render( - , - ) + const { findAllByText } = render() await findAllByText('Must be at least three') }, 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 19/31] 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 176d74a9e9857a87bffc892ffe353db5597e9c2f Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 20:07:14 -0800 Subject: [PATCH 20/31] chore: fix types --- packages/react-form/tests/onblur-detection.bench.tsx | 4 ++-- packages/react-form/tests/onchange-detection.bench.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-form/tests/onblur-detection.bench.tsx b/packages/react-form/tests/onblur-detection.bench.tsx index 91e5718ce..294d704c5 100644 --- a/packages/react-form/tests/onblur-detection.bench.tsx +++ b/packages/react-form/tests/onblur-detection.bench.tsx @@ -101,7 +101,7 @@ function ReactHookFormOnBlurBenchmark() { num: arr, }, mode: 'onBlur', - resolver: zodResolver(validator), + resolver: zodResolver(validator as never), }) return ( @@ -129,7 +129,7 @@ function ReactHookFormHeadlessOnBlurBenchmark() { num: arr, }, mode: 'onBlur', - resolver: zodResolver(validator), + resolver: zodResolver(validator as never), }) return ( diff --git a/packages/react-form/tests/onchange-detection.bench.tsx b/packages/react-form/tests/onchange-detection.bench.tsx index efdbd2dbb..d779039c4 100644 --- a/packages/react-form/tests/onchange-detection.bench.tsx +++ b/packages/react-form/tests/onchange-detection.bench.tsx @@ -101,7 +101,7 @@ function ReactHookFormOnChangeBenchmark() { num: arr, }, mode: 'onChange', - resolver: zodResolver(validator), + resolver: zodResolver(validator as never), }) return ( @@ -129,7 +129,7 @@ function ReactHookFormHeadlessOnChangeBenchmark() { num: arr, }, mode: 'onChange', - resolver: zodResolver(validator), + resolver: zodResolver(validator as never), }) return ( From 8dafa0c7140e22cd65f3eda6331fb657d1055187 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 20:08:07 -0800 Subject: [PATCH 21/31] Revert "chore: another" This reverts commit bfc348db41cdb0fea1d73d145f72911bc578bd5a. # Conflicts: # packages/react-form/src/useField.tsx --- packages/react-form/src/useField.tsx | 84 +++++++++++++++++----------- 1 file changed, 50 insertions(+), 34 deletions(-) diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx index bd7028677..2a3f546cf 100644 --- a/packages/react-form/src/useField.tsx +++ b/packages/react-form/src/useField.tsx @@ -223,32 +223,37 @@ export function useField< // For array mode, only track length changes to avoid re-renders when child properties change // See: https://github.com/TanStack/form/issues/1925 - // Consolidate all field state subscriptions into a single useStore call - // to reduce subscription overhead (1 subscription instead of 7) - const isArrayMode = opts.mode === 'array' - const reactiveState = useStore( + const reactiveStateValue = useStore( fieldApi.store, - (state) => { - return { - value: isArrayMode - ? Object.keys((state.value as unknown) ?? []).length - : state.value, - isTouched: state.meta.isTouched, - isBlurred: state.meta.isBlurred, - isDirty: state.meta.isDirty, - errorMap: state.meta.errorMap, - errorSourceMap: state.meta.errorSourceMap, - isValidating: state.meta.isValidating, - } - }, - (a, b) => - a.value === b.value && - a.isTouched === b.isTouched && - a.isBlurred === b.isBlurred && - a.isDirty === b.isDirty && - a.errorMap === b.errorMap && - a.errorSourceMap === b.errorSourceMap && - a.isValidating === b.isValidating, + (opts.mode === 'array' + ? (state) => Object.keys((state.value as unknown) ?? []).length + : (state) => state.value) as ( + state: typeof fieldApi.state, + ) => TData | number, + ) + const reactiveMetaIsTouched = useStore( + fieldApi.store, + (state) => state.meta.isTouched, + ) + const reactiveMetaIsBlurred = useStore( + fieldApi.store, + (state) => state.meta.isBlurred, + ) + const reactiveMetaIsDirty = useStore( + fieldApi.store, + (state) => state.meta.isDirty, + ) + const reactiveMetaErrorMap = useStore( + fieldApi.store, + (state) => state.meta.errorMap, + ) + const reactiveMetaErrorSourceMap = useStore( + fieldApi.store, + (state) => state.meta.errorSourceMap, + ) + const reactiveMetaIsValidating = useStore( + fieldApi.store, + (state) => state.meta.isValidating, ) // This makes me sad, but if I understand correctly, this is what we have to do for reactivity to work properly with React compiler. @@ -257,18 +262,19 @@ export function useField< ...fieldApi, get state() { return { - // For array mode, reactiveState.value is the length (for reactivity tracking), + // For array mode, reactiveStateValue is the length (for reactivity tracking), // so we need to get the actual value from fieldApi - value: isArrayMode ? fieldApi.state.value : reactiveState.value, + value: + opts.mode === 'array' ? fieldApi.state.value : reactiveStateValue, get meta() { return { ...fieldApi.state.meta, - isTouched: reactiveState.isTouched, - isBlurred: reactiveState.isBlurred, - isDirty: reactiveState.isDirty, - errorMap: reactiveState.errorMap, - errorSourceMap: reactiveState.errorSourceMap, - isValidating: reactiveState.isValidating, + isTouched: reactiveMetaIsTouched, + isBlurred: reactiveMetaIsBlurred, + isDirty: reactiveMetaIsDirty, + errorMap: reactiveMetaErrorMap, + errorSourceMap: reactiveMetaErrorSourceMap, + isValidating: reactiveMetaIsValidating, } satisfies AnyFieldMeta }, } satisfies AnyFieldApi['state'] @@ -318,7 +324,17 @@ export function useField< extendedApi.Field = Field as never return extendedApi - }, [fieldApi, isArrayMode, reactiveState]) + }, [ + fieldApi, + opts.mode, + reactiveStateValue, + reactiveMetaIsTouched, + reactiveMetaIsBlurred, + reactiveMetaIsDirty, + reactiveMetaErrorMap, + reactiveMetaErrorSourceMap, + reactiveMetaIsValidating, + ]) useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi]) From 2a54084b804e618cd3e7609bc3d336efe52ffec4 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 12 Feb 2026 20:34:02 -0800 Subject: [PATCH 22/31] chore: re-fix issues with react ssr --- packages/form-core/src/FieldApi.ts | 29 ++++++++++++++++++++++ packages/form-core/tests/FieldApi.spec.ts | 3 ++- packages/form-core/tests/fieldMeta.spec.ts | 4 ++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index ac7062c64..9b541eb6a 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1380,6 +1380,35 @@ export class FieldApi< value: this.state.value, fieldApi: this, }) + + // Only used so that `transform` works properly in the React adapter after first render + // This can be removed when `transform` is able to use the values directly from FormAPI instead of duplicate them + // @see https://github.com/TanStack/form-v2-private-discussions/blob/001-duplicated-error-map-keys/RFCs/001-duplicated-error-map-keys.md + const unsubFormListener = this.form.baseStore.subscribe(() => { + const perFieldStore = this.form._getOrCreatePerFieldStore( + this.name as string, + this.options.defaultMeta as any, + ) + + if (this.getValue() !== perFieldStore.get().value) { + perFieldStore.setState((prev) => ({ + ...prev, + value: this.getValue(), + })) + } + if ( + this.form._getFieldMetaBase(this.name) !== perFieldStore.get().metaBase + ) { + perFieldStore.setState((prev) => ({ + ...prev, + meta: this.getMeta(), + })) + } + }) + + return () => { + unsubFormListener.unsubscribe() + } } /** diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index f5dcdaa54..d4c9ce3fe 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -1596,7 +1596,8 @@ describe('field api', () => { name: 'name', }) - field.mount() + const unmount = field.mount() + unmount() 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 e1aa59081..33b4b96df 100644 --- a/packages/form-core/tests/fieldMeta.spec.ts +++ b/packages/form-core/tests/fieldMeta.spec.ts @@ -141,12 +141,14 @@ describe('fieldMeta accessing', () => { name: 'name', }) - field.mount() + const cleanup = 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 e6984aac3ab9cc7a8c7e57208864b5381ca2717b Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 05:46:44 +0000 Subject: [PATCH 23/31] 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 24/31] 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, From e877514f3afb5aab55146daf75aeef3223f6a3d4 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 17 Feb 2026 04:02:00 -0800 Subject: [PATCH 25/31] chore: upgrade Zod to v4 everywhere --- examples/angular/standard-schema/package.json | 2 +- examples/lit/standard-schema/package.json | 2 +- examples/react/dynamic/package.json | 2 +- .../next-server-actions-zod/package.json | 2 +- .../next-server-actions-zod/src/app/action.ts | 9 ++++- examples/react/standard-schema/package.json | 2 +- examples/solid/standard-schema/package.json | 2 +- examples/svelte/standard-schema/package.json | 2 +- examples/vue/standard-schema/package.json | 2 +- packages/form-core/package.json | 2 +- packages/form-core/tests/FieldApi.spec.ts | 1 - packages/form-core/tests/FormApi.spec.ts | 1 - pnpm-lock.yaml | 36 +++++++++---------- 13 files changed, 35 insertions(+), 30 deletions(-) diff --git a/examples/angular/standard-schema/package.json b/examples/angular/standard-schema/package.json index 92cca2a27..548b2720b 100644 --- a/examples/angular/standard-schema/package.json +++ b/examples/angular/standard-schema/package.json @@ -23,7 +23,7 @@ "rxjs": "^7.8.2", "tslib": "^2.8.1", "valibot": "^1.1.0", - "zod": "^3.25.76", + "zod": "^4.3.6", "zone.js": "0.15.1" }, "devDependencies": { diff --git a/examples/lit/standard-schema/package.json b/examples/lit/standard-schema/package.json index 50f692578..37a716aec 100644 --- a/examples/lit/standard-schema/package.json +++ b/examples/lit/standard-schema/package.json @@ -14,7 +14,7 @@ "effect": "^3.17.14", "lit": "^3.3.1", "valibot": "^1.1.0", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "devDependencies": { "vite": "^7.2.2" diff --git a/examples/react/dynamic/package.json b/examples/react/dynamic/package.json index 28d48b9d8..b2f6ac14c 100644 --- a/examples/react/dynamic/package.json +++ b/examples/react/dynamic/package.json @@ -20,7 +20,7 @@ "@types/react-dom": "^19.0.3", "@vitejs/plugin-react": "^5.1.1", "vite": "^7.2.2", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "browserslist": { "production": [ diff --git a/examples/react/next-server-actions-zod/package.json b/examples/react/next-server-actions-zod/package.json index 20297d27a..37d08c2ea 100644 --- a/examples/react/next-server-actions-zod/package.json +++ b/examples/react/next-server-actions-zod/package.json @@ -13,7 +13,7 @@ "next": "16.0.5", "react": "^19.0.0", "react-dom": "^19.0.0", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "devDependencies": { "@types/node": "^24.1.0", diff --git a/examples/react/next-server-actions-zod/src/app/action.ts b/examples/react/next-server-actions-zod/src/app/action.ts index 76ce9e669..21e3bc0fd 100644 --- a/examples/react/next-server-actions-zod/src/app/action.ts +++ b/examples/react/next-server-actions-zod/src/app/action.ts @@ -7,8 +7,15 @@ import { import { z } from 'zod' import { formOpts } from './shared-code' +// Required as `z.coerce.number()` defined the type as `unknown`, so we need to do the coercion and validation manually +const zodAtLeast12 = z + .custom() + .refine((value) => Number.isFinite(Number(value)), 'Invalid number') + .transform((value) => Number(value)) + .refine((value) => value >= 12, 'Age must be at least 12') + const schema = z.object({ - age: z.coerce.number().min(12), + age: zodAtLeast12, firstName: z.string(), }) diff --git a/examples/react/standard-schema/package.json b/examples/react/standard-schema/package.json index 6c138f5c7..989b59a8f 100644 --- a/examples/react/standard-schema/package.json +++ b/examples/react/standard-schema/package.json @@ -17,7 +17,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "valibot": "^1.1.0", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "devDependencies": { "@types/react": "^19.0.7", diff --git a/examples/solid/standard-schema/package.json b/examples/solid/standard-schema/package.json index d34709fa9..e991d2dc4 100644 --- a/examples/solid/standard-schema/package.json +++ b/examples/solid/standard-schema/package.json @@ -16,7 +16,7 @@ "react-dom": "^19.0.0", "solid-js": "^1.9.9", "valibot": "^1.1.0", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "devDependencies": { "typescript": "5.8.2", diff --git a/examples/svelte/standard-schema/package.json b/examples/svelte/standard-schema/package.json index e78749dc9..a1125b3f2 100644 --- a/examples/svelte/standard-schema/package.json +++ b/examples/svelte/standard-schema/package.json @@ -13,7 +13,7 @@ "arktype": "^2.1.22", "effect": "^3.17.14", "valibot": "^1.1.0", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.1.1", diff --git a/examples/vue/standard-schema/package.json b/examples/vue/standard-schema/package.json index 5fc0e6107..669f795e5 100644 --- a/examples/vue/standard-schema/package.json +++ b/examples/vue/standard-schema/package.json @@ -17,7 +17,7 @@ "react-dom": "^19.0.0", "valibot": "^1.1.0", "vue": "^3.5.13", - "zod": "^3.25.76" + "zod": "^4.3.6" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.4", diff --git a/packages/form-core/package.json b/packages/form-core/package.json index 6e3338de9..b91af7e0c 100644 --- a/packages/form-core/package.json +++ b/packages/form-core/package.json @@ -58,6 +58,6 @@ "devDependencies": { "arktype": "^2.1.22", "valibot": "^1.1.0", - "zod": "^3.25.76" + "zod": "^4.3.6" } } diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index e8d18bff5..4d8c148fa 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -2251,7 +2251,6 @@ describe('field api', () => { it('should throw an error when passing an async Standard Schema to parseValueWithSchema', async () => { const testSchema = z.string().superRefine(async () => { await sleep(1000) - return true }) const form = new FormApi({ diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index c1b36a85c..5f4affee5 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -3446,7 +3446,6 @@ describe('form api', () => { }) .superRefine(async () => { await sleep(1000) - return true }) const form = new FormApi({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4527d2ea8..9295f9bb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -300,8 +300,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(typescript@5.8.2) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 zone.js: specifier: 0.15.1 version: 0.15.1 @@ -363,8 +363,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(typescript@5.9.3) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 devDependencies: vite: specifier: ^7.2.2 @@ -510,8 +510,8 @@ importers: specifier: ^7.2.2 version: 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) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 examples/react/field-errors-from-form-validators: dependencies: @@ -624,8 +624,8 @@ importers: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@types/node': specifier: ^24.1.0 @@ -778,8 +778,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(typescript@5.9.3) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@types/react': specifier: ^19.0.7 @@ -1013,8 +1013,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(typescript@5.8.2) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 devDependencies: typescript: specifier: 5.8.2 @@ -1107,8 +1107,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(typescript@5.8.2) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 @@ -1194,8 +1194,8 @@ importers: specifier: ^3.5.13 version: 3.5.16(typescript@5.8.2) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.4 @@ -1281,8 +1281,8 @@ importers: specifier: ^1.1.0 version: 1.1.0(typescript@5.9.3) zod: - specifier: ^3.25.76 - version: 3.25.76 + specifier: ^4.3.6 + version: 4.3.6 packages/form-devtools: dependencies: From e01ae496333b3b048b1dd4c7f0431d40a6a1d44e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:13:47 +0000 Subject: [PATCH 26/31] ci: apply automated fixes and generate docs --- packages/form-core/src/FieldApi.ts | 4 ++-- packages/form-core/src/FormApi.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index a01bb1c47..b74882c68 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1,4 +1,4 @@ -import { batch, createStore} from '@tanstack/store' +import { batch, createStore } from '@tanstack/store' import { isStandardSchemaValidator, standardSchemaValidators, @@ -13,7 +13,7 @@ import { mergeOpts, } from './utils' import { defaultValidationLogic } from './ValidationLogic' -import type {ReadonlyStore} from '@tanstack/store'; +import type { ReadonlyStore } from '@tanstack/store' import type { DeepKeys, DeepValue, UnwrapOneLevelOfArray } from './util-types' import type { StandardSchemaV1, diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index f939562c5..4844f8970 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1,4 +1,4 @@ -import { batch, createStore} from '@tanstack/store' +import { batch, createStore } from '@tanstack/store' import { deleteBy, determineFormLevelErrorSourceAndValue, @@ -23,7 +23,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' @@ -2417,7 +2417,6 @@ export class FormApi< isTouched: true, isDirty: true, errorMap: { - ...prevMeta?.errorMap, onMount: undefined, }, From 35c156e7b57d8bb278a30a1e796a5421804f393b Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 17 Feb 2026 04:45:04 -0800 Subject: [PATCH 27/31] chore: fix tests --- packages/form-core/src/FieldApi.ts | 55 +++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index a01bb1c47..c1187067c 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1172,6 +1172,10 @@ export class FieldApi< let prevRawValue: unknown = undefined let cachedMeta: AnyFieldMeta | undefined = undefined + // The type assertion is needed because when consumed from adapter packages + // (e.g. solid-form), duplicate @tanstack/store installations cause + // TypeScript to see separate declarations of the private 'atom' property + // in Store vs ReadonlyStore, making structural assignment fail. this.store = createStore( ( prevVal: @@ -1205,7 +1209,15 @@ export class FieldApi< this.name as string, opts.defaultMeta as any, ) - const { value: rawValue, metaBase } = perFieldStore.get() + const perFieldState = perFieldStore.get() + // Guard against undefined state — can occur when the reactive system + // calls _update() without arguments on a mutable atom after it gets + // unwatched during re-entrant flush cycles (e.g. array field removal + // in frameworks with synchronous reactive updates like Solid). + if (!perFieldState) { + return prevVal as any + } + const { value: rawValue, metaBase } = perFieldState // Compute derived meta with caching let meta: AnyFieldMeta @@ -1303,7 +1315,7 @@ export class FieldApi< TFormOnDynamicAsync > }, - ) + ) as any } /** @@ -1390,18 +1402,43 @@ export class FieldApi< this.options.defaultMeta as any, ) - if (this.getValue() !== perFieldStore.get().value) { + const currentState = perFieldStore.get() + // Guard: during re-entrant flush cycles (e.g. array field removal in + // frameworks with synchronous reactive updates like Solid), the + // per-field store's atom snapshot can be corrupted to undefined. + // Skip the sync — the field is being cleaned up. + if (!currentState) { + return + } + + const currentValue = this.getValue() + const currentMetaBase = this.form._getFieldMetaBase(this.name) + + // Compute what needs updating BEFORE calling setState. + // Avoid calling this.getMeta() inside setState updaters — it accesses + // this.store.state which triggers the computed store to recompute, + // reading perFieldStore.get() while the per-field store's atom is + // mid-update. This corrupts the reactive graph in frameworks with + // synchronous reactive systems (e.g. Solid). + const needsValueUpdate = currentValue !== currentState.value + const needsMetaUpdate = currentMetaBase !== currentState.metaBase + + if (needsValueUpdate && needsMetaUpdate) { + // Combine both updates into a single setState to avoid double-flush perFieldStore.setState((prev) => ({ ...prev, - value: this.getValue(), + value: currentValue, + metaBase: currentMetaBase ?? prev.metaBase, })) - } - if ( - this.form._getFieldMetaBase(this.name) !== perFieldStore.get().metaBase - ) { + } else if (needsValueUpdate) { + perFieldStore.setState((prev) => ({ + ...prev, + value: currentValue, + })) + } else if (needsMetaUpdate) { perFieldStore.setState((prev) => ({ ...prev, - meta: this.getMeta(), + metaBase: currentMetaBase ?? prev.metaBase, })) } }) From e1cfea2a49fd807a4bd419fca5f0464ec0d02d16 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 17 Feb 2026 04:55:19 -0800 Subject: [PATCH 28/31] chore: fix perf for non-transform users --- packages/form-core/src/FieldApi.ts | 417 +++++++++++++++-------------- 1 file changed, 212 insertions(+), 205 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 422e67735..cdcd4f1d1 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -49,16 +49,16 @@ type FieldErrorMapFromValidator< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, > = Partial< Record< DeepKeys, @@ -130,8 +130,8 @@ type UnwrapFormValidateOrFnForInner< > = [TValidateOrFn] extends [FormValidateFn] ? ReturnType : [TValidateOrFn] extends [StandardSchemaV1] - ? StandardBrandedSchemaV1 - : undefined + ? StandardBrandedSchemaV1 + : undefined export type UnwrapFieldValidateOrFn< TName extends string, @@ -139,27 +139,27 @@ export type UnwrapFieldValidateOrFn< TFormValidateOrFn extends undefined | FormValidateOrFn, > = | ([TFormValidateOrFn] extends [StandardSchemaV1] - ? TName extends keyof TStandardOut - ? StandardSchemaV1Issue[] - : undefined - : undefined) + ? TName extends keyof TStandardOut + ? StandardSchemaV1Issue[] + : undefined + : undefined) | (UnwrapFormValidateOrFnForInner extends infer TFormValidateVal - ? TFormValidateVal extends { __standardSchemaV1: true } - ? [DeepValue] extends [never] - ? undefined - : StandardSchemaV1Issue[] - : TFormValidateVal extends { fields: any } - ? TName extends keyof TFormValidateVal['fields'] - ? TFormValidateVal['fields'][TName] - : undefined - : undefined - : never) + ? TFormValidateVal extends { __standardSchemaV1: true } + ? [DeepValue] extends [never] + ? undefined + : StandardSchemaV1Issue[] + : TFormValidateVal extends { fields: any } + ? TName extends keyof TFormValidateVal['fields'] + ? TFormValidateVal['fields'][TName] + : undefined + : undefined + : never) | ([TValidateOrFn] extends [FieldValidateFn] - ? ReturnType - : [TValidateOrFn] extends [StandardSchemaV1] - ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][] - StandardSchemaV1Issue[] - : undefined) + ? ReturnType + : [TValidateOrFn] extends [StandardSchemaV1] + ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][] + StandardSchemaV1Issue[] + : undefined) /** * @private @@ -216,8 +216,8 @@ type UnwrapFormAsyncValidateOrFnForInner< > = [TValidateOrFn] extends [FormValidateAsyncFn] ? Awaited> : [TValidateOrFn] extends [StandardSchemaV1] - ? StandardBrandedSchemaV1 - : undefined + ? StandardBrandedSchemaV1 + : undefined export type UnwrapFieldAsyncValidateOrFn< TName extends string, @@ -225,27 +225,27 @@ export type UnwrapFieldAsyncValidateOrFn< TFormValidateOrFn extends undefined | FormAsyncValidateOrFn, > = | ([TFormValidateOrFn] extends [StandardSchemaV1] - ? TName extends keyof TStandardOut - ? StandardSchemaV1Issue[] - : undefined - : undefined) + ? TName extends keyof TStandardOut + ? StandardSchemaV1Issue[] + : undefined + : undefined) | (UnwrapFormAsyncValidateOrFnForInner extends infer TFormValidateVal - ? TFormValidateVal extends { __standardSchemaV1: true } - ? [DeepValue] extends [never] - ? undefined - : StandardSchemaV1Issue[] - : TFormValidateVal extends { fields: any } - ? TName extends keyof TFormValidateVal['fields'] - ? TFormValidateVal['fields'][TName] - : undefined - : undefined - : never) + ? TFormValidateVal extends { __standardSchemaV1: true } + ? [DeepValue] extends [never] + ? undefined + : StandardSchemaV1Issue[] + : TFormValidateVal extends { fields: any } + ? TName extends keyof TFormValidateVal['fields'] + ? TFormValidateVal['fields'][TName] + : undefined + : undefined + : never) | ([TValidateOrFn] extends [FieldValidateAsyncFn] - ? Awaited> - : [TValidateOrFn] extends [StandardSchemaV1] - ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][] - StandardSchemaV1Issue[] - : undefined) + ? Awaited> + : [TValidateOrFn] extends [StandardSchemaV1] + ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][] + StandardSchemaV1Issue[] + : undefined) /** * @private @@ -292,20 +292,20 @@ export interface FieldValidators< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, > { /** * An optional function, that runs on the mount event of input. @@ -396,20 +396,20 @@ export interface FieldOptions< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, > { /** * The field name. The type will be `DeepKeys` to ensure your name is a deep key of the parent dataset. @@ -490,49 +490,49 @@ export interface FieldApiOptions< in out TName extends DeepKeys, in out TData extends DeepValue, in out TOnMount extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnChange extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnBlur extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnSubmit extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnDynamic extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TFormOnMount extends undefined | FormValidateOrFn, in out TFormOnChange extends undefined | FormValidateOrFn, in out TFormOnChangeAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnBlur extends undefined | FormValidateOrFn, in out TFormOnBlurAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnSubmit extends undefined | FormValidateOrFn, in out TFormOnSubmitAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnDynamic extends undefined | FormValidateOrFn, in out TFormOnDynamicAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnServer extends undefined | FormAsyncValidateOrFn, in out TParentSubmitMeta, > extends FieldOptions< @@ -572,20 +572,20 @@ export type FieldMetaBase< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TFormOnMount extends undefined | FormValidateOrFn, TFormOnChange extends undefined | FormValidateOrFn, TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, @@ -664,20 +664,20 @@ export type FieldMetaDerived< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TFormOnMount extends undefined | FormValidateOrFn, TFormOnChange extends undefined | FormValidateOrFn, TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, @@ -693,36 +693,36 @@ export type FieldMetaDerived< */ errors: Array< | UnwrapOneLevelOfArray< - UnwrapFieldValidateOrFn - > + UnwrapFieldValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldValidateOrFn - > + UnwrapFieldValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldAsyncValidateOrFn - > + UnwrapFieldAsyncValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldValidateOrFn - > + UnwrapFieldValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldAsyncValidateOrFn - > + UnwrapFieldAsyncValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldValidateOrFn - > + UnwrapFieldValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldAsyncValidateOrFn - > + UnwrapFieldAsyncValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldValidateOrFn - > + UnwrapFieldValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldAsyncValidateOrFn< - TName, - TOnDynamicAsync, - TFormOnDynamicAsync - > + UnwrapFieldAsyncValidateOrFn< + TName, + TOnDynamicAsync, + TFormOnDynamicAsync > + > > /** * A flag that is `true` if the field's value has not been modified by the user. Opposite of `isDirty`. @@ -772,20 +772,20 @@ export type FieldMeta< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TFormOnMount extends undefined | FormValidateOrFn, TFormOnChange extends undefined | FormValidateOrFn, TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, @@ -876,20 +876,20 @@ export type FieldState< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TFormOnMount extends undefined | FormValidateOrFn, TFormOnChange extends undefined | FormValidateOrFn, TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, @@ -982,49 +982,49 @@ export class FieldApi< in out TName extends DeepKeys, in out TData extends DeepValue, in out TOnMount extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnChange extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnBlur extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnSubmit extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnDynamic extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TFormOnMount extends undefined | FormValidateOrFn, in out TFormOnChange extends undefined | FormValidateOrFn, in out TFormOnChangeAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnBlur extends undefined | FormValidateOrFn, in out TFormOnBlurAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnSubmit extends undefined | FormValidateOrFn, in out TFormOnSubmitAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnDynamic extends undefined | FormValidateOrFn, in out TFormOnDynamicAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnServer extends undefined | FormAsyncValidateOrFn, in out TParentSubmitMeta, > { @@ -1180,28 +1180,28 @@ export class FieldApi< ( prevVal: | FieldState< - TParentData, - TName, - TData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TFormOnMount, - TFormOnChange, - TFormOnChangeAsync, - TFormOnBlur, - TFormOnBlurAsync, - TFormOnSubmit, - TFormOnSubmitAsync, - TFormOnDynamic, - TFormOnDynamicAsync - > + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync + > | undefined, ) => { // Subscribe to per-field store for fine-grained reactivity (O(1) instead of O(N)) @@ -1328,8 +1328,8 @@ export class FieldApi< TType extends 'validate' | 'validateAsync', >(props: { validate: TType extends 'validate' - ? FieldValidateOrFn - : FieldAsyncValidateOrFn + ? FieldValidateOrFn + : FieldAsyncValidateOrFn value: TValue type: TType // When `api` is 'field', the return type cannot be `FormValidationError` @@ -1393,6 +1393,13 @@ export class FieldApi< fieldApi: this, }) + // Turns out the code below unfortunately slows down forms by a LOT, so we just give the perf back to the user if they're not using `transform` + if (!this.form.options.transform) { + return () => { + // noop + } + } + // Only used so that `transform` works properly in the React adapter after first render // This can be removed when `transform` is able to use the values directly from FormAPI instead of duplicate them // @see https://github.com/TanStack/form-v2-private-discussions/blob/001-duplicated-error-map-keys/RFCs/001-duplicated-error-map-keys.md @@ -1732,7 +1739,7 @@ export class FieldApi< field.form.options.validationLogic || defaultValidationLogic, }) fieldValidates.forEach((validate) => { - ;(validate as any).field = field + ; (validate as any).field = field }) return acc.concat(fieldValidates as never) }, @@ -1755,16 +1762,16 @@ export class FieldApi< const fieldLevelError = validateObj.validate ? normalizeError( - field.runValidator({ - validate: validateObj.validate, - value: { - value: field.store.state.value, - validationSource: 'field', - fieldApi: field, - }, - type: 'validate', - }), - ) + field.runValidator({ + validate: validateObj.validate, + value: { + value: field.store.state.value, + validationSource: 'field', + fieldApi: field, + }, + type: 'validate', + }), + ) : undefined const formLevelError = errorFromForm[errorMapKey] @@ -1871,7 +1878,7 @@ export class FieldApi< field.form.options.validationLogic || defaultValidationLogic, }) fieldValidates.forEach((validate) => { - ;(validate as any).field = field + ; (validate as any).field = field }) return acc.concat(fieldValidates as never) }, From 8e95bbc6f6ea29fcf59d198f4e3699b41e196122 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:56:09 +0000 Subject: [PATCH 29/31] ci: apply automated fixes and generate docs --- packages/form-core/src/FieldApi.ts | 410 ++++++++++++++--------------- 1 file changed, 205 insertions(+), 205 deletions(-) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index cdcd4f1d1..09fcb7e08 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -49,16 +49,16 @@ type FieldErrorMapFromValidator< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, > = Partial< Record< DeepKeys, @@ -130,8 +130,8 @@ type UnwrapFormValidateOrFnForInner< > = [TValidateOrFn] extends [FormValidateFn] ? ReturnType : [TValidateOrFn] extends [StandardSchemaV1] - ? StandardBrandedSchemaV1 - : undefined + ? StandardBrandedSchemaV1 + : undefined export type UnwrapFieldValidateOrFn< TName extends string, @@ -139,27 +139,27 @@ export type UnwrapFieldValidateOrFn< TFormValidateOrFn extends undefined | FormValidateOrFn, > = | ([TFormValidateOrFn] extends [StandardSchemaV1] - ? TName extends keyof TStandardOut - ? StandardSchemaV1Issue[] - : undefined - : undefined) + ? TName extends keyof TStandardOut + ? StandardSchemaV1Issue[] + : undefined + : undefined) | (UnwrapFormValidateOrFnForInner extends infer TFormValidateVal - ? TFormValidateVal extends { __standardSchemaV1: true } - ? [DeepValue] extends [never] - ? undefined - : StandardSchemaV1Issue[] - : TFormValidateVal extends { fields: any } - ? TName extends keyof TFormValidateVal['fields'] - ? TFormValidateVal['fields'][TName] - : undefined - : undefined - : never) + ? TFormValidateVal extends { __standardSchemaV1: true } + ? [DeepValue] extends [never] + ? undefined + : StandardSchemaV1Issue[] + : TFormValidateVal extends { fields: any } + ? TName extends keyof TFormValidateVal['fields'] + ? TFormValidateVal['fields'][TName] + : undefined + : undefined + : never) | ([TValidateOrFn] extends [FieldValidateFn] - ? ReturnType - : [TValidateOrFn] extends [StandardSchemaV1] - ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][] - StandardSchemaV1Issue[] - : undefined) + ? ReturnType + : [TValidateOrFn] extends [StandardSchemaV1] + ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][] + StandardSchemaV1Issue[] + : undefined) /** * @private @@ -216,8 +216,8 @@ type UnwrapFormAsyncValidateOrFnForInner< > = [TValidateOrFn] extends [FormValidateAsyncFn] ? Awaited> : [TValidateOrFn] extends [StandardSchemaV1] - ? StandardBrandedSchemaV1 - : undefined + ? StandardBrandedSchemaV1 + : undefined export type UnwrapFieldAsyncValidateOrFn< TName extends string, @@ -225,27 +225,27 @@ export type UnwrapFieldAsyncValidateOrFn< TFormValidateOrFn extends undefined | FormAsyncValidateOrFn, > = | ([TFormValidateOrFn] extends [StandardSchemaV1] - ? TName extends keyof TStandardOut - ? StandardSchemaV1Issue[] - : undefined - : undefined) + ? TName extends keyof TStandardOut + ? StandardSchemaV1Issue[] + : undefined + : undefined) | (UnwrapFormAsyncValidateOrFnForInner extends infer TFormValidateVal - ? TFormValidateVal extends { __standardSchemaV1: true } - ? [DeepValue] extends [never] - ? undefined - : StandardSchemaV1Issue[] - : TFormValidateVal extends { fields: any } - ? TName extends keyof TFormValidateVal['fields'] - ? TFormValidateVal['fields'][TName] - : undefined - : undefined - : never) + ? TFormValidateVal extends { __standardSchemaV1: true } + ? [DeepValue] extends [never] + ? undefined + : StandardSchemaV1Issue[] + : TFormValidateVal extends { fields: any } + ? TName extends keyof TFormValidateVal['fields'] + ? TFormValidateVal['fields'][TName] + : undefined + : undefined + : never) | ([TValidateOrFn] extends [FieldValidateAsyncFn] - ? Awaited> - : [TValidateOrFn] extends [StandardSchemaV1] - ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][] - StandardSchemaV1Issue[] - : undefined) + ? Awaited> + : [TValidateOrFn] extends [StandardSchemaV1] + ? // TODO: Check if `disableErrorFlat` is enabled, if so, return StandardSchemaV1Issue[][] + StandardSchemaV1Issue[] + : undefined) /** * @private @@ -292,20 +292,20 @@ export interface FieldValidators< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, > { /** * An optional function, that runs on the mount event of input. @@ -396,20 +396,20 @@ export interface FieldOptions< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, > { /** * The field name. The type will be `DeepKeys` to ensure your name is a deep key of the parent dataset. @@ -490,49 +490,49 @@ export interface FieldApiOptions< in out TName extends DeepKeys, in out TData extends DeepValue, in out TOnMount extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnChange extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnBlur extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnSubmit extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnDynamic extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TFormOnMount extends undefined | FormValidateOrFn, in out TFormOnChange extends undefined | FormValidateOrFn, in out TFormOnChangeAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnBlur extends undefined | FormValidateOrFn, in out TFormOnBlurAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnSubmit extends undefined | FormValidateOrFn, in out TFormOnSubmitAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnDynamic extends undefined | FormValidateOrFn, in out TFormOnDynamicAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnServer extends undefined | FormAsyncValidateOrFn, in out TParentSubmitMeta, > extends FieldOptions< @@ -572,20 +572,20 @@ export type FieldMetaBase< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TFormOnMount extends undefined | FormValidateOrFn, TFormOnChange extends undefined | FormValidateOrFn, TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, @@ -664,20 +664,20 @@ export type FieldMetaDerived< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TFormOnMount extends undefined | FormValidateOrFn, TFormOnChange extends undefined | FormValidateOrFn, TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, @@ -693,36 +693,36 @@ export type FieldMetaDerived< */ errors: Array< | UnwrapOneLevelOfArray< - UnwrapFieldValidateOrFn - > + UnwrapFieldValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldValidateOrFn - > + UnwrapFieldValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldAsyncValidateOrFn - > + UnwrapFieldAsyncValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldValidateOrFn - > + UnwrapFieldValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldAsyncValidateOrFn - > + UnwrapFieldAsyncValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldValidateOrFn - > + UnwrapFieldValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldAsyncValidateOrFn - > + UnwrapFieldAsyncValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldValidateOrFn - > + UnwrapFieldValidateOrFn + > | UnwrapOneLevelOfArray< - UnwrapFieldAsyncValidateOrFn< - TName, - TOnDynamicAsync, - TFormOnDynamicAsync + UnwrapFieldAsyncValidateOrFn< + TName, + TOnDynamicAsync, + TFormOnDynamicAsync + > > - > > /** * A flag that is `true` if the field's value has not been modified by the user. Opposite of `isDirty`. @@ -772,20 +772,20 @@ export type FieldMeta< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TFormOnMount extends undefined | FormValidateOrFn, TFormOnChange extends undefined | FormValidateOrFn, TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, @@ -876,20 +876,20 @@ export type FieldState< TOnMount extends undefined | FieldValidateOrFn, TOnChange extends undefined | FieldValidateOrFn, TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnBlur extends undefined | FieldValidateOrFn, TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnSubmit extends undefined | FieldValidateOrFn, TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TOnDynamic extends undefined | FieldValidateOrFn, TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, TFormOnMount extends undefined | FormValidateOrFn, TFormOnChange extends undefined | FormValidateOrFn, TFormOnChangeAsync extends undefined | FormAsyncValidateOrFn, @@ -982,49 +982,49 @@ export class FieldApi< in out TName extends DeepKeys, in out TData extends DeepValue, in out TOnMount extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnChange extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnChangeAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnBlur extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnBlurAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnSubmit extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnSubmitAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TOnDynamic extends - | undefined - | FieldValidateOrFn, + | undefined + | FieldValidateOrFn, in out TOnDynamicAsync extends - | undefined - | FieldAsyncValidateOrFn, + | undefined + | FieldAsyncValidateOrFn, in out TFormOnMount extends undefined | FormValidateOrFn, in out TFormOnChange extends undefined | FormValidateOrFn, in out TFormOnChangeAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnBlur extends undefined | FormValidateOrFn, in out TFormOnBlurAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnSubmit extends undefined | FormValidateOrFn, in out TFormOnSubmitAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnDynamic extends undefined | FormValidateOrFn, in out TFormOnDynamicAsync extends - | undefined - | FormAsyncValidateOrFn, + | undefined + | FormAsyncValidateOrFn, in out TFormOnServer extends undefined | FormAsyncValidateOrFn, in out TParentSubmitMeta, > { @@ -1180,28 +1180,28 @@ export class FieldApi< ( prevVal: | FieldState< - TParentData, - TName, - TData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TFormOnMount, - TFormOnChange, - TFormOnChangeAsync, - TFormOnBlur, - TFormOnBlurAsync, - TFormOnSubmit, - TFormOnSubmitAsync, - TFormOnDynamic, - TFormOnDynamicAsync - > + TParentData, + TName, + TData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TFormOnMount, + TFormOnChange, + TFormOnChangeAsync, + TFormOnBlur, + TFormOnBlurAsync, + TFormOnSubmit, + TFormOnSubmitAsync, + TFormOnDynamic, + TFormOnDynamicAsync + > | undefined, ) => { // Subscribe to per-field store for fine-grained reactivity (O(1) instead of O(N)) @@ -1328,8 +1328,8 @@ export class FieldApi< TType extends 'validate' | 'validateAsync', >(props: { validate: TType extends 'validate' - ? FieldValidateOrFn - : FieldAsyncValidateOrFn + ? FieldValidateOrFn + : FieldAsyncValidateOrFn value: TValue type: TType // When `api` is 'field', the return type cannot be `FormValidationError` @@ -1739,7 +1739,7 @@ export class FieldApi< field.form.options.validationLogic || defaultValidationLogic, }) fieldValidates.forEach((validate) => { - ; (validate as any).field = field + ;(validate as any).field = field }) return acc.concat(fieldValidates as never) }, @@ -1762,16 +1762,16 @@ export class FieldApi< const fieldLevelError = validateObj.validate ? normalizeError( - field.runValidator({ - validate: validateObj.validate, - value: { - value: field.store.state.value, - validationSource: 'field', - fieldApi: field, - }, - type: 'validate', - }), - ) + field.runValidator({ + validate: validateObj.validate, + value: { + value: field.store.state.value, + validationSource: 'field', + fieldApi: field, + }, + type: 'validate', + }), + ) : undefined const formLevelError = errorFromForm[errorMapKey] @@ -1878,7 +1878,7 @@ export class FieldApi< field.form.options.validationLogic || defaultValidationLogic, }) fieldValidates.forEach((validate) => { - ; (validate as any).field = field + ;(validate as any).field = field }) return acc.concat(fieldValidates as never) }, From 7494b7f906b7e9109f074bd5314e73963392a120 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 17 Feb 2026 05:03:31 -0800 Subject: [PATCH 30/31] chore: fix lint --- packages/form-core/src/FieldApi.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 09fcb7e08..505b074df 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1214,6 +1214,7 @@ export class FieldApi< // calls _update() without arguments on a mutable atom after it gets // unwatched during re-entrant flush cycles (e.g. array field removal // in frameworks with synchronous reactive updates like Solid). + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!perFieldState) { return prevVal as any } @@ -1414,6 +1415,7 @@ export class FieldApi< // frameworks with synchronous reactive updates like Solid), the // per-field store's atom snapshot can be corrupted to undefined. // Skip the sync — the field is being cleaned up. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!currentState) { return } From e982049bfe57c148ea234d3f969276ce3bcbea24 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Tue, 17 Feb 2026 05:04:57 -0800 Subject: [PATCH 31/31] chore: fix Nextjs build --- .../next-server-actions-zod/src/app/client-component.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/react/next-server-actions-zod/src/app/client-component.tsx b/examples/react/next-server-actions-zod/src/app/client-component.tsx index d7511a649..b3ad5701f 100644 --- a/examples/react/next-server-actions-zod/src/app/client-component.tsx +++ b/examples/react/next-server-actions-zod/src/app/client-component.tsx @@ -11,6 +11,13 @@ import { z } from 'zod' import someAction from './action' import { formOpts } from './shared-code' +// Required as `z.coerce.number()` defined the type as `unknown`, so we need to do the coercion and validation manually +const zodAtLeast8 = z + .custom() + .refine((value) => Number.isFinite(Number(value)), 'Invalid number') + .transform((value) => Number(value)) + .refine((value) => value >= 8, 'Age must be at least 8') + export const ClientComp = () => { const [state, action] = useActionState(someAction, initialFormState) @@ -27,7 +34,7 @@ export const ClientComp = () => { {(field) => {