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 4db1c5f1a..37d08c2ea 100644 --- a/examples/react/next-server-actions-zod/package.json +++ b/examples/react/next-server-actions-zod/package.json @@ -9,11 +9,11 @@ }, "dependencies": { "@tanstack/react-form-nextjs": "^1.28.3", - "@tanstack/react-store": "^0.8.1", + "@tanstack/react-store": "^0.9.1", "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/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) => { diff --git a/examples/react/next-server-actions/package.json b/examples/react/next-server-actions/package.json index 6ab2dc9de..31549bde7 100644 --- a/examples/react/next-server-actions/package.json +++ b/examples/react/next-server-actions/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@tanstack/react-form-nextjs": "^1.28.3", - "@tanstack/react-store": "^0.8.1", + "@tanstack/react-store": "^0.9.1", "next": "16.0.5", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/examples/react/remix/package.json b/examples/react/remix/package.json index 524d3115b..b7c4b56d2 100644 --- a/examples/react/remix/package.json +++ b/examples/react/remix/package.json @@ -12,7 +12,7 @@ "@remix-run/react": "^2.17.1", "@remix-run/serve": "^2.17.1", "@tanstack/react-form-remix": "^1.28.3", - "@tanstack/react-store": "^0.8.1", + "@tanstack/react-store": "^0.9.1", "isbot": "^5.1.30", "react": "^19.0.0", "react-dom": "^19.0.0" 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/react/tanstack-start/package.json b/examples/react/tanstack-start/package.json index 473bc9a16..bd1757cd3 100644 --- a/examples/react/tanstack-start/package.json +++ b/examples/react/tanstack-start/package.json @@ -14,7 +14,7 @@ "@tanstack/react-form-start": "^1.28.3", "@tanstack/react-router": "^1.134.9", "@tanstack/react-start": "^1.134.9", - "@tanstack/react-store": "^0.8.1", + "@tanstack/react-store": "^0.9.1", "react": "^19.0.0", "react-dom": "^19.0.0" }, 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/angular-form/package.json b/packages/angular-form/package.json index df2180d16..f75ed4abd 100644 --- a/packages/angular-form/package.json +++ b/packages/angular-form/package.json @@ -42,7 +42,7 @@ "src" ], "dependencies": { - "@tanstack/angular-store": "^0.8.1", + "@tanstack/angular-store": "^0.9.1", "@tanstack/form-core": "workspace:*", "tslib": "^2.8.1" }, diff --git a/packages/form-core/package.json b/packages/form-core/package.json index 6424cb144..b91af7e0c 100644 --- a/packages/form-core/package.json +++ b/packages/form-core/package.json @@ -53,11 +53,11 @@ "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/pacer-lite": "^0.1.1", - "@tanstack/store": "^0.8.1" + "@tanstack/store": "^0.9.1" }, "devDependencies": { "arktype": "^2.1.22", "valibot": "^1.1.0", - "zod": "^3.25.76" + "zod": "^4.3.6" } } diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 1d5ba3096..505b074df 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1,18 +1,19 @@ -import { Derived, batch } from '@tanstack/store' +import { batch, createStore } from '@tanstack/store' import { isStandardSchemaValidator, standardSchemaValidators, } from './standardSchemaValidator' -import { defaultFieldMeta } from './metaHelper' import { determineFieldLevelErrorSourceAndValue, evaluate, getAsyncValidatorArray, getBy, getSyncValidatorArray, + isNonEmptyArray, mergeOpts, } from './utils' import { defaultValidationLogic } from './ValidationLogic' +import type { ReadonlyStore } from '@tanstack/store' import type { DeepKeys, DeepValue, UnwrapOneLevelOfArray } from './util-types' import type { StandardSchemaV1, @@ -1090,7 +1091,7 @@ export class FieldApi< /** * The field state store. */ - store!: Derived< + store!: ReadonlyStore< FieldState< TParentData, TName, @@ -1167,10 +1168,17 @@ export class FieldApi< formListeners: {} as Record, } - this.store = new Derived({ - deps: [this.form.store], - fn: ({ prevVal: _prevVal }) => { - const prevVal = _prevVal as + let prevMetaBase: AnyFieldMetaBase | undefined = undefined + 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: | FieldState< TParentData, TName, @@ -1194,14 +1202,80 @@ export class FieldApi< TFormOnDynamic, TFormOnDynamicAsync > - | undefined + | undefined, + ) => { + // 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 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). + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!perFieldState) { + return prevVal as any + } + const { value: rawValue, metaBase } = perFieldState + + // Compute derived meta with caching + let meta: AnyFieldMeta + 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 meta = this.form.getFieldMeta(this.name) ?? { - ...defaultFieldMeta, - ...opts.defaultMeta, + 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, + } + } + + prevMetaBase = metaBase + prevRawValue = rawValue + cachedMeta = meta } - let value = this.form.getFieldValue(this.name) + let value = rawValue if ( !meta.isTouched && (value as unknown) === undefined && @@ -1242,7 +1316,7 @@ export class FieldApi< TFormOnDynamicAsync > }, - }) + ) as any } /** @@ -1275,8 +1349,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, @@ -1322,7 +1394,67 @@ export class FieldApi< fieldApi: this, }) - return cleanup + // 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 + const unsubFormListener = this.form.baseStore.subscribe(() => { + const perFieldStore = this.form._getOrCreatePerFieldStore( + this.name as string, + this.options.defaultMeta as any, + ) + + 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. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + 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: currentValue, + metaBase: currentMetaBase ?? prev.metaBase, + })) + } else if (needsValueUpdate) { + perFieldStore.setState((prev) => ({ + ...prev, + value: currentValue, + })) + } else if (needsMetaUpdate) { + perFieldStore.setState((prev) => ({ + ...prev, + metaBase: currentMetaBase ?? prev.metaBase, + })) + } + }) + + return () => { + unsubFormListener.unsubscribe() + } } /** @@ -1370,7 +1502,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/FieldGroupApi.ts b/packages/form-core/src/FieldGroupApi.ts index e69a29fc9..6cd9d0976 100644 --- a/packages/form-core/src/FieldGroupApi.ts +++ b/packages/form-core/src/FieldGroupApi.ts @@ -1,5 +1,6 @@ -import { Derived } from '@tanstack/store' +import { createStore } from '@tanstack/store' import { concatenatePaths, getBy, makePathArray } from './utils' +import type { ReadonlyStore } from '@tanstack/store' import type { Updater } from './utils' import type { FormApi, @@ -225,7 +226,7 @@ export class FieldGroupApi< return newProps } - store: Derived> + store: ReadonlyStore> get state() { return this.store.state @@ -275,38 +276,35 @@ 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 + return () => {} } /** diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index f1b5abde4..4844f8970 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 { batch, createStore } from '@tanstack/store' import { deleteBy, determineFormLevelErrorSourceAndValue, @@ -6,6 +6,8 @@ import { functionalUpdate, getAsyncValidatorArray, getBy, + getChildPaths, + getParentPaths, getSyncValidatorArray, isGlobalFormValidationError, isNonEmptyArray, @@ -21,6 +23,7 @@ import { } from './standardSchemaValidator' import { defaultFieldMeta, metaHelper } from './metaHelper' import { formEventClient } from './EventClient' +import type { ReadonlyStore, Store } from '@tanstack/store' // types import type { ValidationLogicFn } from './ValidationLogic' @@ -891,22 +894,7 @@ export class FormApi< TOnServer > > - fieldMetaDerived: Derived< - FormState< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer - >['fieldMeta'] - > - store: Derived< + store: ReadonlyStore< FormState< TFormData, TOnMount, @@ -926,6 +914,16 @@ 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< + string, + Store<{ value: any; metaBase: AnyFieldMetaBase }> + > = {} as any + get state() { return this.store.state } @@ -1036,20 +1034,10 @@ export class FormApi< } } - this.baseStore = new Store(baseStoreVal) + this.baseStore = createStore(baseStoreVal) as never - 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] - - let originalMetaCount = 0 - - const fieldMeta: FormState< + let prevBaseStoreForStore: + | BaseFormState< TFormData, TOnMount, TOnChange, @@ -1061,245 +1049,248 @@ export class FormApi< TOnDynamic, TOnDynamicAsync, TOnServer - >['fieldMeta'] = {} - - for (const fieldName of Object.keys( - currBaseStore.fieldMetaBase, - ) as Array) { - const currBaseMeta = currBaseStore.fieldMetaBase[ - fieldName as never - ] as AnyFieldMetaBase - - const prevBaseMeta = prevBaseStore?.fieldMetaBase[ - fieldName as never - ] as AnyFieldMetaBase | undefined - - const prevFieldInfo = - prevVal?.[fieldName as never as keyof typeof prevVal] - - const curFieldVal = getBy(currBaseStore.values, fieldName) + > + | undefined = 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, - ) + this.store = createStore< + FormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + > + >((prevVal) => { + const currBaseStore = this.baseStore.get() - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const fieldInstance = this.getFieldInfo(fieldName)?.instance + // Derive fieldMeta from baseStore.fieldMetaBase (inlined from former fieldMetaDerived) + let originalMetaCount = 0 + const prevFieldMetaObj = prevVal?.fieldMeta - if (!fieldInstance || !fieldInstance.options.disableErrorFlat) { - fieldErrors = fieldErrors.flat(1) - } - } + const currFieldMeta: FormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + >['fieldMeta'] = {} + + for (const fieldName of Object.keys(currBaseStore.fieldMetaBase) as Array< + keyof typeof currBaseStore.fieldMetaBase + >) { + const currBaseMeta = currBaseStore.fieldMetaBase[ + fieldName as never + ] as AnyFieldMetaBase + + const prevBaseMeta = prevBaseStoreForStore?.fieldMetaBase[ + fieldName as never + ] as AnyFieldMetaBase | undefined + + const prevFieldInfo = + prevFieldMetaObj?.[ + fieldName as never as keyof typeof prevFieldMetaObj + ] + + const curFieldVal = getBy(currBaseStore.values, fieldName) + + 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 } - return fieldMeta - }, - }) - - 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[] + currFieldMeta[fieldName] = { + ...currBaseMeta, + errors: fieldErrors ?? [], + isPristine: isFieldPristine, + isValid: isFieldValid, + isDefaultValue: isDefaultValue, + } satisfies AnyFieldMeta as AnyFieldMeta + } - const isFieldsValidating = fieldMetaValues.some( - (field) => field.isValidating, - ) + const fieldMetaBaseKeyCount = Object.keys( + currBaseStore.fieldMetaBase, + ).length + const fieldMetaUnchanged = + fieldMetaBaseKeyCount > 0 && + prevFieldMetaObj && + originalMetaCount === fieldMetaBaseKeyCount + + // 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 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 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 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 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 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 + 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) + // 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 }) - } + 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 - } + if ( + prevVal && + prevBaseStoreForStore && + prevVal.errorMap === errorMap && + fieldMetaUnchanged && + 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 + } - const 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 - > + const state = { + ...currBaseStore, + errorMap, + fieldMeta: fieldMetaUnchanged ? prevFieldMetaObj! : currFieldMeta, + errors, + isFieldsValidating, + isFieldsValid, + isFormValid, + isValid, + canSubmit, + isTouched, + isBlurred, + isPristine, + isDefaultValue, + isDirty, + } as FormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + > - return state - }, + prevBaseStoreForStore = this.baseStore.get() + + return state }) this.handleSubmit = this.handleSubmit.bind(this) @@ -1311,6 +1302,82 @@ 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 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): void => { + const newValues = this.baseStore.state.values + + const updateStoreValue = (path: string) => { + const store = this._perFieldStores[path] + if (store) { + store.setState((prev) => ({ + ...prev, + value: getBy(newValues, path), + })) + } + } + + // Update parent paths (e.g., for 'items[0].name', update 'items' and 'items[0]') + for (const parentPath of getParentPaths(field)) { + updateStoreValue(parentPath) + } + + // Update child paths (e.g., for 'items', update 'items[0]', 'items[0].name', etc.) + for (const childPath of getChildPaths( + field, + Object.keys(this._perFieldStores), + )) { + updateStoreValue(childPath) + } + } + + /** + * @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 | undefined) ?? { ...defaultFieldMeta }, + })) + } + } + /** * @private */ @@ -1337,9 +1404,6 @@ export class FormApi< } mount = () => { - const cleanupFieldMetaDerived = this.fieldMetaDerived.mount() - const cleanupStoreDerived = this.store.mount() - // devtool broadcasts const cleanupDevtoolBroadcast = this.store.subscribe(() => { throttleFormState(this) @@ -1383,9 +1447,7 @@ export class FormApi< cleanupFormForceSubmitListener() cleanupFormResetListener() cleanupFormStateListener() - cleanupDevtoolBroadcast() - cleanupFieldMetaDerived() - cleanupStoreDerived() + cleanupDevtoolBroadcast.unsubscribe() // broadcast form unmount for devtools formEventClient.emit('form-unmounted', { @@ -1468,6 +1530,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, @@ -1503,6 +1568,9 @@ export class FormApi< fieldMetaBase, }), ) + + // Sync all per-field stores after reset + this._syncAllPerFieldStores() } /** @@ -1657,7 +1725,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', }, @@ -1669,10 +1737,13 @@ 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[]) + // 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 && @@ -1681,11 +1752,12 @@ 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] @@ -1710,22 +1782,44 @@ 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, + })) + } } } // 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: { @@ -1747,7 +1841,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 ) { @@ -1767,7 +1861,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 ) { @@ -1809,8 +1903,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 })) } /** @@ -1823,15 +1917,17 @@ export class FormApi< | Partial, ValidationError>> | undefined + const pendingAsyncMetaChanges: Record = {} + 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, } @@ -1850,7 +1946,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, @@ -1880,19 +1976,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] @@ -1910,17 +2006,38 @@ 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, + })) + } } } @@ -2185,7 +2302,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. @@ -2196,6 +2313,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. */ @@ -2223,18 +2353,26 @@ 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!, + })) + } } /** @@ -2265,28 +2403,58 @@ export class FormApi< const dontRunListeners = opts?.dontRunListeners ?? false const dontValidate = opts?.dontValidate ?? false - batch(() => { + // 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) { - this.setFieldMeta(field, (prev) => ({ - ...prev, + const prevMeta = prev.fieldMetaBase[field] as + | AnyFieldMetaBase + | undefined + newFieldMeta = { + ...prevMeta, isTouched: true, isDirty: true, errorMap: { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - ...prev?.errorMap, + ...prevMeta?.errorMap, onMount: undefined, }, - })) - } - - this.baseStore.setState((prev) => { + } as AnyFieldMetaBase return { ...prev, - values: setBy(prev.values, field, updater), + values: newValues, + fieldMetaBase: { + ...prev.fieldMetaBase, + [field]: newFieldMeta, + }, } - }) + } + return { + ...prev, + values: newValues, + } }) + // 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) + if (!dontRunListeners) { this.getFieldInfo(field).instance?.triggerOnChangeListener() } @@ -2315,6 +2483,34 @@ 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) { + for (const parentPath of getParentPaths(f as string)) { + const parentStore = this._perFieldStores[parentPath] + if (parentStore) { + parentStore.setState((prev) => ({ + ...prev, + value: getBy(newValues, parentPath), + })) + } + } + } } /** @@ -2550,6 +2746,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 }, + })) + } } /** diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index 71a6ddb69..03108ca04 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -89,6 +89,41 @@ 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 +} + +/** + * 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 diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index d4c9ce3fe..4d8c148fa 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 () => { @@ -2249,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/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/react-form/package.json b/packages/react-form/package.json index eac7680ad..b5953801d 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" @@ -52,16 +53,22 @@ ], "dependencies": { "@tanstack/form-core": "workspace:*", - "@tanstack/react-store": "^0.8.1" + "@tanstack/react-store": "^0.9.1" }, "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/src/useForm.tsx b/packages/react-form/src/useForm.tsx index b4ce691e6..2d75f0d77 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/onblur-detection.bench.tsx b/packages/react-form/tests/onblur-detection.bench.tsx new file mode 100644 index 000000000..294d704c5 --- /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 as never), + }) + + return ( + <> + {arr.map((_num, i) => { + return ( +
+ + +
+ ) + })} + + ) +} + +function ReactHookFormHeadlessOnBlurBenchmark() { + const { control, handleSubmit } = useReactHookForm({ + defaultValues: { + num: arr, + }, + mode: 'onBlur', + resolver: zodResolver(validator as never), + }) + + 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() + } + }, + }, + ) +}) 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..d779039c4 --- /dev/null +++ b/packages/react-form/tests/onchange-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 TanStackFormOnChangeBenchmark() { + const form = useTanStackForm({ + defaultValues: { num: arr }, + validators: { + onChange: 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 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(validator as never), + }) + + return ( + <> + {arr.map((_num, i) => { + return ( +
+ + +
+ ) + })} + + ) +} + +function ReactHookFormHeadlessOnChangeBenchmark() { + const { control, handleSubmit } = useReactHookForm({ + defaultValues: { + num: arr, + }, + mode: 'onChange', + resolver: zodResolver(validator as never), + }) + + 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/tests/onmount-detection.bench.tsx b/packages/react-form/tests/onmount-detection.bench.tsx new file mode 100644 index 000000000..d1113fb43 --- /dev/null +++ b/packages/react-form/tests/onmount-detection.bench.tsx @@ -0,0 +1,124 @@ +/* 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() + } + }, + }, + ) +}) diff --git a/packages/react-form/tests/useForm.test.tsx b/packages/react-form/tests/useForm.test.tsx index 36daaaa51..ce6733fcb 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/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/packages/solid-form/package.json b/packages/solid-form/package.json index 006721276..bd54753ff 100644 --- a/packages/solid-form/package.json +++ b/packages/solid-form/package.json @@ -56,7 +56,7 @@ ], "dependencies": { "@tanstack/form-core": "workspace:*", - "@tanstack/solid-store": "^0.8.1" + "@tanstack/solid-store": "^0.9.1" }, "devDependencies": { "solid-js": "^1.9.9", 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/svelte-form/package.json b/packages/svelte-form/package.json index 7cef4b42a..c75f4a8a4 100644 --- a/packages/svelte-form/package.json +++ b/packages/svelte-form/package.json @@ -41,7 +41,7 @@ ], "dependencies": { "@tanstack/form-core": "workspace:*", - "@tanstack/svelte-store": "^0.9.1" + "@tanstack/svelte-store": "^0.10.1" }, "devDependencies": { "@sveltejs/package": "^2.5.3", diff --git a/packages/vue-form/package.json b/packages/vue-form/package.json index b6a994031..92dbad82e 100644 --- a/packages/vue-form/package.json +++ b/packages/vue-form/package.json @@ -53,7 +53,7 @@ ], "dependencies": { "@tanstack/form-core": "workspace:*", - "@tanstack/vue-store": "^0.8.1" + "@tanstack/vue-store": "^0.9.1" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.4", 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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 498efb24a..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: @@ -581,8 +581,8 @@ importers: specifier: ^1.28.3 version: link:../../../packages/react-form-nextjs '@tanstack/react-store': - specifier: ^0.8.1 - version: 0.8.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^0.9.1 + version: 0.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: specifier: 16.0.5 version: 16.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0) @@ -612,8 +612,8 @@ importers: specifier: ^1.28.3 version: link:../../../packages/react-form-nextjs '@tanstack/react-store': - specifier: ^0.8.1 - version: 0.8.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^0.9.1 + version: 0.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: specifier: 16.0.5 version: 16.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0) @@ -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 @@ -689,8 +689,8 @@ importers: specifier: ^1.28.3 version: link:../../../packages/react-form-remix '@tanstack/react-store': - specifier: ^0.8.1 - version: 0.8.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^0.9.1 + version: 0.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) isbot: specifier: ^5.1.30 version: 5.1.31 @@ -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 @@ -812,8 +812,8 @@ importers: specifier: ^1.134.9 version: 1.135.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(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)))(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)) '@tanstack/react-store': - specifier: ^0.8.1 - version: 0.8.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^0.9.1 + version: 0.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: specifier: ^19.0.0 version: 19.1.0 @@ -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 @@ -1213,8 +1213,8 @@ importers: packages/angular-form: dependencies: '@tanstack/angular-store': - specifier: ^0.8.1 - version: 0.8.1(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)) + specifier: ^0.9.1 + version: 0.9.1(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1)) '@tanstack/form-core': specifier: workspace:* version: link:../form-core @@ -1271,8 +1271,8 @@ importers: specifier: ^0.1.1 version: 0.1.1 '@tanstack/store': - specifier: ^0.8.1 - version: 0.8.1 + specifier: ^0.9.1 + version: 0.9.1 devDependencies: arktype: specifier: ^2.1.22 @@ -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: @@ -1331,9 +1331,15 @@ importers: specifier: workspace:* version: link:../form-core '@tanstack/react-store': - specifier: ^0.8.1 - version: 0.8.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^0.9.1 + version: 0.9.1(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: @@ -1480,8 +1498,8 @@ importers: specifier: workspace:* version: link:../form-core '@tanstack/solid-store': - specifier: ^0.8.1 - version: 0.8.1(solid-js@1.9.9) + specifier: ^0.9.1 + version: 0.9.1(solid-js@1.9.9) devDependencies: solid-js: specifier: ^1.9.9 @@ -1515,8 +1533,8 @@ importers: specifier: workspace:* version: link:../form-core '@tanstack/svelte-store': - specifier: ^0.9.1 - version: 0.9.1(svelte@5.41.1) + specifier: ^0.10.1 + version: 0.10.1(svelte@5.41.1) devDependencies: '@sveltejs/package': specifier: ^2.5.3 @@ -1540,8 +1558,8 @@ importers: specifier: workspace:* version: link:../form-core '@tanstack/vue-store': - specifier: ^0.8.1 - version: 0.8.1(vue@3.5.16(typescript@5.9.3)) + specifier: ^0.9.1 + version: 0.9.1(vue@3.5.16(typescript@5.9.3)) devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.4 @@ -3139,6 +3157,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'} @@ -4658,6 +4688,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} @@ -4773,8 +4806,8 @@ packages: '@swc/types@0.1.24': resolution: {integrity: sha512-tjTMh3V4vAORHtdTprLlfoMptu1WfTZG9Rsca6yOKyNYsRr+MUXutKmliB17orgSZk5DpnDxs8GUdd/qwYxOng==} - '@tanstack/angular-store@0.8.1': - resolution: {integrity: sha512-Zb8e1QVeBoSu/s1R3fXczctEqB7lZrdPL87/9INwCaRSY3jPqNn3SlzP8yvwvBwv7axaFgfUrhQJXlnACC3Vnw==} + '@tanstack/angular-store@0.9.1': + resolution: {integrity: sha512-XdrVBZperSRulkk8kLsPP/apNZQZwAWvNeO6PMb+kRv7iOXAzxaIK2LQTZFLtfT1QgQZFeEqU8klJcdcuG6JcQ==} peerDependencies: '@angular/common': '>=19.0.0' '@angular/core': '>=19.0.0' @@ -4914,6 +4947,12 @@ 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@0.9.1': + resolution: {integrity: sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA==} + 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'} @@ -4957,8 +4996,8 @@ packages: peerDependencies: solid-js: '>=1.9.7' - '@tanstack/solid-store@0.8.1': - resolution: {integrity: sha512-1p4TTJGIZJ2J7130aTo7oWfHVRSCd9DxLP3HzcDMnzn56pz8krlyBEzsE+z/sHGXP0EC/JjT02fgj2L9+fmf8Q==} + '@tanstack/solid-store@0.9.1': + resolution: {integrity: sha512-gx7ToM+Yrkui36NIj0HjAufzv1Dg8usjtVFy5H3Ll52Xjuz+eliIJL+ihAr4LRuWh3nDPBR+nCLW0ShFrbE5yw==} peerDependencies: solid-js: ^1.6.0 @@ -4983,8 +5022,11 @@ packages: '@tanstack/store@0.8.1': resolution: {integrity: sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw==} - '@tanstack/svelte-store@0.9.1': - resolution: {integrity: sha512-4RYp0CXSB9tjlUZNl29mjraWeRquKzuaW+bGGI4s3kS6BWatgt7BfX4OtoLT8MTBdepW9ARwqHZ3s8YGpfOZkQ==} + '@tanstack/store@0.9.1': + resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==} + + '@tanstack/svelte-store@0.10.1': + resolution: {integrity: sha512-heeyV9bZQHbEJyJ7oWegQXmcyA8NSPP58JsZgRpvf8+lwEMfX+MW1IvPJbGZqmH+poULAz7DDxjC4JEe7l57LA==} peerDependencies: svelte: ^5.0.0 @@ -5000,8 +5042,8 @@ packages: resolution: {integrity: sha512-FOl8EF6SAcljanKSm5aBeJaflFcxQAytTbxtNW8HC6D4x+UBW68IC4tBcrlrsI0wXHBmC/Gz4Ovvv8qCtiXSgQ==} engines: {node: '>=18'} - '@tanstack/vue-store@0.8.1': - resolution: {integrity: sha512-37rNptEo86+2Jm2kTLvgVtboRRwHkksjxCKCrdl73eeZIU0jU34ZMP/ayd5+bCCo6epdbrqcb13gjUBSGp4Blg==} + '@tanstack/vue-store@0.9.1': + resolution: {integrity: sha512-mXXZzPWom656MExX2gG1fqopJhToDbqGEl98WtJ5/hyouQHtQXiAgtsPNLzUcVcwU9okM/OCWv7QAgXf6C5ziQ==} peerDependencies: '@vue/composition-api': ^1.2.1 vue: ^2.5.0 || ^3.0.0 @@ -5170,6 +5212,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==} @@ -6390,6 +6437,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'} @@ -7073,6 +7124,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'} @@ -7952,6 +8008,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==} @@ -9102,6 +9161,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==} @@ -10822,6 +10890,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: @@ -10836,8 +10910,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==} @@ -12618,7 +12692,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 @@ -12630,7 +12704,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 @@ -12720,6 +12794,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': @@ -14148,6 +14233,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)) @@ -14258,11 +14345,11 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/angular-store@0.8.1(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))': + '@tanstack/angular-store@0.9.1(@angular/common@20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))': dependencies: '@angular/common': 20.3.6(@angular/core@20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) '@angular/core': 20.3.6(@angular/compiler@20.3.6)(rxjs@7.8.2)(zone.js@0.15.1) - '@tanstack/store': 0.8.1 + '@tanstack/store': 0.9.1 tslib: 2.8.1 '@tanstack/devtools-client@0.0.3': @@ -14460,6 +14547,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@0.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/store': 0.9.1 + 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 @@ -14545,9 +14639,9 @@ snapshots: - csstype - utf-8-validate - '@tanstack/solid-store@0.8.1(solid-js@1.9.9)': + '@tanstack/solid-store@0.9.1(solid-js@1.9.9)': dependencies: - '@tanstack/store': 0.8.1 + '@tanstack/store': 0.9.1 solid-js: 1.9.9 '@tanstack/start-client-core@1.135.2': @@ -14608,9 +14702,11 @@ snapshots: '@tanstack/store@0.8.1': {} - '@tanstack/svelte-store@0.9.1(svelte@5.41.1)': + '@tanstack/store@0.9.1': {} + + '@tanstack/svelte-store@0.10.1(svelte@5.41.1)': dependencies: - '@tanstack/store': 0.8.1 + '@tanstack/store': 0.9.1 svelte: 5.41.1 '@tanstack/typedoc-config@0.3.1(typescript@5.8.2)': @@ -14636,9 +14732,9 @@ snapshots: - typescript - vite - '@tanstack/vue-store@0.8.1(vue@3.5.16(typescript@5.9.3))': + '@tanstack/vue-store@0.9.1(vue@3.5.16(typescript@5.9.3))': dependencies: - '@tanstack/store': 0.8.1 + '@tanstack/store': 0.9.1 vue: 3.5.16(typescript@5.9.3) vue-demi: 0.14.10(vue@3.5.16(typescript@5.9.3)) @@ -14845,6 +14941,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': @@ -16283,6 +16384,8 @@ snapshots: deep-object-diff@1.1.9: {} + deepmerge@2.2.1: {} + deepmerge@4.3.1: {} default-browser-id@5.0.0: {} @@ -17161,6 +17264,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: {} @@ -17927,7 +18044,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: {} @@ -18050,6 +18167,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.17.23: {} + lodash.camelcase@4.3.0: {} lodash.debounce@4.0.8: {} @@ -19511,6 +19630,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: {} @@ -21395,6 +21520,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 @@ -21405,7 +21535,7 @@ snapshots: zod@3.25.76: {} - zod@4.1.12: {} + zod@4.3.6: {} zone.js@0.15.1: {}