From be7f9bf74dec3157bd07544e1fb4948c36922f42 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 29 Dec 2024 08:11:57 -0800 Subject: [PATCH] feat: make accessing field properties faster and more stable * chore: WIP migration to derived state * chore!: temporarily remove broken tests * ci: apply automated fixes and generate docs * chore: refactor derived state in FormApi to use Derived class * ci: apply automated fixes and generate docs * chore: fix build * ci: apply automated fixes and generate docs * Revert "chore: fix build" This reverts commit 4087d87f873a65c35ee94d6823eefd442036729d. * ci: apply automated fixes and generate docs * chore: migrate to form mount on every test * chore: simplify complex tests * ci: apply automated fixes and generate docs * chore: refactor all derived state into `FormApi`, even field derived values * ci: apply automated fixes and generate docs * chore: fix build * fix: only regenerate field.errors when field.errorMap has changed * chore: fix tests * ci: apply automated fixes and generate docs * chore: add previously failing test * chore: only recompute errors when prevDepVal is different * ci: apply automated fixes and generate docs * chore: WIP validate new Store API * chore: fix issues with tests * ci: apply automated fixes and generate docs * chore: fix build * ci: apply automated fixes and generate docs * fix: all tests now pass in Form Core and React Form * chore: bump Store to 0.7 * ci: apply automated fixes and generate docs * chore: fix issues with Sherif, fix mount tests * ci: apply automated fixes and generate docs * test: add previously failing useStore test * ci: apply automated fixes and generate docs * chore: fix timing with Solid Form * ci: apply automated fixes and generate docs * chore: key rename and fix comment * ci: apply automated fixes and generate docs * chore: fix eslint --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../react/reference/functions/usestore.md | 50 ++- .../solid/reference/functions/field.md | 2 +- .../solid/reference/functions/usestore.md | 50 ++- .../reference/type-aliases/fieldcomponent.md | 2 +- .../vue/reference/functions/usestore.md | 50 ++- docs/reference/classes/fieldapi.md | 74 ++-- docs/reference/classes/formapi.md | 104 +++-- docs/reference/index.md | 4 + docs/reference/type-aliases/baseformstate.md | 94 ++++ .../type-aliases/derivedformstate.md | 116 +++++ docs/reference/type-aliases/fieldmeta.md | 62 +-- docs/reference/type-aliases/fieldmetabase.md | 56 +++ .../type-aliases/fieldmetaderived.md | 32 ++ docs/reference/type-aliases/fieldstate.md | 2 +- docs/reference/type-aliases/formstate.md | 168 +------ .../react/next-server-actions/package.json | 2 +- examples/react/remix/package.json | 2 +- examples/react/tanstack-start/package.json | 2 +- packages/angular-form/package.json | 2 +- packages/form-core/package.json | 2 +- packages/form-core/src/FieldApi.ts | 123 ++---- packages/form-core/src/FormApi.ts | 409 +++++++++++------- packages/form-core/tests/FieldApi.spec.ts | 130 +++++- packages/form-core/tests/FormApi.spec.ts | 290 ++----------- packages/react-form/package.json | 2 +- packages/react-form/tests/useForm.test.tsx | 32 ++ packages/solid-form/package.json | 2 +- packages/solid-form/src/createField.tsx | 19 +- packages/vue-form/package.json | 2 +- pnpm-lock.yaml | 89 ++-- 30 files changed, 1084 insertions(+), 890 deletions(-) create mode 100644 docs/reference/type-aliases/baseformstate.md create mode 100644 docs/reference/type-aliases/derivedformstate.md create mode 100644 docs/reference/type-aliases/fieldmetabase.md create mode 100644 docs/reference/type-aliases/fieldmetaderived.md diff --git a/docs/framework/react/reference/functions/usestore.md b/docs/framework/react/reference/functions/usestore.md index 966b0b77a..45ed3f214 100644 --- a/docs/framework/react/reference/functions/usestore.md +++ b/docs/framework/react/reference/functions/usestore.md @@ -5,32 +5,62 @@ title: useStore # Function: useStore() +## Call Signature + ```ts -function useStore(store, selector?): TSelected +function useStore(store, selector?): TSelected ``` -## Type Parameters +### Type Parameters • **TState** • **TSelected** = `NoInfer`\<`TState`\> -• **TUpdater** *extends* `AnyUpdater` = `AnyUpdater` +### Parameters + +#### store + +`Store`\<`TState`, `any`\> + +#### selector? + +(`state`) => `TSelected` + +### Returns + +`TSelected` + +### Defined in + +node\_modules/.pnpm/@tanstack+react-store@0.7.0\_react-dom@18.3.1\_react@18.3.1\_\_react@18.3.1/node\_modules/@tanstack/react-store/dist/esm/index.d.ts:7 + +## Call Signature + +```ts +function useStore(store, selector?): TSelected +``` + +### Type Parameters + +• **TState** + +• **TSelected** = `NoInfer`\<`TState`\> -## Parameters +### Parameters -### store +#### store -`Store`\<`TState`, `TUpdater`\> +`Derived`\<`TState`, `any`\> -### selector? +#### selector? (`state`) => `TSelected` -## Returns +### Returns `TSelected` -## Defined in +### Defined in -node\_modules/.pnpm/@tanstack+react-store@0.6.1\_react-dom@18.3.1\_react@18.3.1\_\_react@18.3.1/node\_modules/@tanstack/react-store/dist/esm/index.d.ts:7 +node\_modules/.pnpm/@tanstack+react-store@0.7.0\_react-dom@18.3.1\_react@18.3.1\_\_react@18.3.1/node\_modules/@tanstack/react-store/dist/esm/index.d.ts:8 diff --git a/docs/framework/solid/reference/functions/field.md b/docs/framework/solid/reference/functions/field.md index 51ec0c8a9..6613b3e90 100644 --- a/docs/framework/solid/reference/functions/field.md +++ b/docs/framework/solid/reference/functions/field.md @@ -33,4 +33,4 @@ function Field(props ## Defined in -[packages/solid-form/src/createField.tsx:185](https://github.com/TanStack/form/blob/main/packages/solid-form/src/createField.tsx#L185) +[packages/solid-form/src/createField.tsx:196](https://github.com/TanStack/form/blob/main/packages/solid-form/src/createField.tsx#L196) diff --git a/docs/framework/solid/reference/functions/usestore.md b/docs/framework/solid/reference/functions/usestore.md index ccb30601b..69e1e4e37 100644 --- a/docs/framework/solid/reference/functions/usestore.md +++ b/docs/framework/solid/reference/functions/usestore.md @@ -5,32 +5,62 @@ title: useStore # Function: useStore() +## Call Signature + ```ts -function useStore(store, selector?): Accessor +function useStore(store, selector?): Accessor ``` -## Type Parameters +### Type Parameters • **TState** • **TSelected** = `NoInfer`\<`TState`\> -• **TUpdater** *extends* `AnyUpdater` = `AnyUpdater` +### Parameters + +#### store + +`Store`\<`TState`, `any`\> + +#### selector? + +(`state`) => `TSelected` + +### Returns + +`Accessor`\<`TSelected`\> + +### Defined in + +node\_modules/.pnpm/@tanstack+solid-store@0.7.0\_solid-js@1.9.3/node\_modules/@tanstack/solid-store/dist/esm/index.d.ts:8 + +## Call Signature + +```ts +function useStore(store, selector?): Accessor +``` + +### Type Parameters + +• **TState** + +• **TSelected** = `NoInfer`\<`TState`\> -## Parameters +### Parameters -### store +#### store -`Store`\<`TState`, `TUpdater`\> +`Derived`\<`TState`, `any`\> -### selector? +#### selector? (`state`) => `TSelected` -## Returns +### Returns `Accessor`\<`TSelected`\> -## Defined in +### Defined in -node\_modules/.pnpm/@tanstack+solid-store@0.6.0\_solid-js@1.9.3/node\_modules/@tanstack/solid-store/dist/esm/index.d.ts:8 +node\_modules/.pnpm/@tanstack+solid-store@0.7.0\_solid-js@1.9.3/node\_modules/@tanstack/solid-store/dist/esm/index.d.ts:9 diff --git a/docs/framework/solid/reference/type-aliases/fieldcomponent.md b/docs/framework/solid/reference/type-aliases/fieldcomponent.md index ad43276ca..d953d3f8e 100644 --- a/docs/framework/solid/reference/type-aliases/fieldcomponent.md +++ b/docs/framework/solid/reference/type-aliases/fieldcomponent.md @@ -41,4 +41,4 @@ type FieldComponent: (store, selector?): Readonly> +function useStore(store, selector?): Readonly> ``` -## Type Parameters +### Type Parameters • **TState** • **TSelected** = `NoInfer`\<`TState`\> -• **TUpdater** *extends* `AnyUpdater` = `AnyUpdater` +### Parameters + +#### store + +`Store`\<`TState`, `any`\> + +#### selector? + +(`state`) => `TSelected` + +### Returns + +`Readonly`\<`Ref`\<`TSelected`\>\> + +### Defined in + +node\_modules/.pnpm/@tanstack+vue-store@0.7.0\_vue@3.5.12\_typescript@5.7.2\_/node\_modules/@tanstack/vue-store/dist/esm/index.d.ts:8 + +## Call Signature + +```ts +function useStore(store, selector?): Readonly> +``` + +### Type Parameters + +• **TState** + +• **TSelected** = `NoInfer`\<`TState`\> -## Parameters +### Parameters -### store +#### store -`Store`\<`TState`, `TUpdater`\> +`Derived`\<`TState`, `any`\> -### selector? +#### selector? (`state`) => `TSelected` -## Returns +### Returns `Readonly`\<`Ref`\<`TSelected`\>\> -## Defined in +### Defined in -node\_modules/.pnpm/@tanstack+vue-store@0.6.0\_vue@3.5.12\_typescript@5.7.2\_/node\_modules/@tanstack/vue-store/dist/esm/index.d.ts:8 +node\_modules/.pnpm/@tanstack+vue-store@0.7.0\_vue@3.5.12\_typescript@5.7.2\_/node\_modules/@tanstack/vue-store/dist/esm/index.d.ts:9 diff --git a/docs/reference/classes/fieldapi.md b/docs/reference/classes/fieldapi.md index a43f532a9..ee7290043 100644 --- a/docs/reference/classes/fieldapi.md +++ b/docs/reference/classes/fieldapi.md @@ -47,7 +47,7 @@ Initializes a new `FieldApi` instance. #### Defined in -[packages/form-core/src/FieldApi.ts:474](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L474) +[packages/form-core/src/FieldApi.ts:477](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L477) ## Properties @@ -61,7 +61,7 @@ A reference to the form API instance. #### Defined in -[packages/form-core/src/FieldApi.ts:436](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L436) +[packages/form-core/src/FieldApi.ts:441](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L441) *** @@ -75,7 +75,7 @@ The field name. #### Defined in -[packages/form-core/src/FieldApi.ts:446](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L446) +[packages/form-core/src/FieldApi.ts:451](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L451) *** @@ -89,44 +89,50 @@ The field options. #### Defined in -[packages/form-core/src/FieldApi.ts:450](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L450) +[packages/form-core/src/FieldApi.ts:455](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L455) *** -### state +### store ```ts -state: FieldState; +store: Derived, readonly any[]>; ``` -The current field state. +The field state store. #### Defined in -[packages/form-core/src/FieldApi.ts:464](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L464) +[packages/form-core/src/FieldApi.ts:465](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L465) *** -### store +### timeoutIds ```ts -store: Store, (cb) => FieldState>; +timeoutIds: Record; ``` -The field state store. - #### Defined in -[packages/form-core/src/FieldApi.ts:460](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L460) +[packages/form-core/src/FieldApi.ts:472](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L472) -*** +## Accessors -### timeoutIds +### state + +#### Get Signature ```ts -timeoutIds: Record; +get state(): FieldState ``` +The current field state. + +##### Returns + +[`FieldState`](../type-aliases/fieldstate.md)\<`TData`\> + #### Defined in [packages/form-core/src/FieldApi.ts:469](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L469) @@ -147,7 +153,7 @@ Gets the field information object. #### Defined in -[packages/form-core/src/FieldApi.ts:713](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L713) +[packages/form-core/src/FieldApi.ts:672](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L672) *** @@ -157,15 +163,13 @@ Gets the field information object. getMeta(): FieldMeta ``` -Gets the current field metadata. - #### Returns [`FieldMeta`](../type-aliases/fieldmeta.md) #### Defined in -[packages/form-core/src/FieldApi.ts:691](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L691) +[packages/form-core/src/FieldApi.ts:661](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L661) *** @@ -187,7 +191,7 @@ Use `field.state.value` instead. #### Defined in -[packages/form-core/src/FieldApi.ts:665](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L665) +[packages/form-core/src/FieldApi.ts:643](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L643) *** @@ -205,7 +209,7 @@ Handles the blur event. #### Defined in -[packages/form-core/src/FieldApi.ts:1065](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1065) +[packages/form-core/src/FieldApi.ts:1024](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1024) *** @@ -229,7 +233,7 @@ Handles the change event. #### Defined in -[packages/form-core/src/FieldApi.ts:1058](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1058) +[packages/form-core/src/FieldApi.ts:1017](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1017) *** @@ -264,7 +268,7 @@ Inserts a value at the specified index, shifting the subsequent values to the ri #### Defined in -[packages/form-core/src/FieldApi.ts:726](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L726) +[packages/form-core/src/FieldApi.ts:685](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L685) *** @@ -286,7 +290,7 @@ Mounts the field instance to the form. #### Defined in -[packages/form-core/src/FieldApi.ts:575](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L575) +[packages/form-core/src/FieldApi.ts:567](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L567) *** @@ -321,7 +325,7 @@ Moves the value at the first specified index to the second specified index. #### Defined in -[packages/form-core/src/FieldApi.ts:756](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L756) +[packages/form-core/src/FieldApi.ts:715](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L715) *** @@ -349,7 +353,7 @@ Pushes a new value to the field. #### Defined in -[packages/form-core/src/FieldApi.ts:718](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L718) +[packages/form-core/src/FieldApi.ts:677](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L677) *** @@ -377,7 +381,7 @@ Removes a value at the specified index. #### Defined in -[packages/form-core/src/FieldApi.ts:744](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L744) +[packages/form-core/src/FieldApi.ts:703](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L703) *** @@ -412,7 +416,7 @@ Replaces a value at the specified index. #### Defined in -[packages/form-core/src/FieldApi.ts:735](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L735) +[packages/form-core/src/FieldApi.ts:694](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L694) *** @@ -436,7 +440,7 @@ Updates the field's errorMap #### Defined in -[packages/form-core/src/FieldApi.ts:1085](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1085) +[packages/form-core/src/FieldApi.ts:1044](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1044) *** @@ -460,7 +464,7 @@ Sets the field metadata. #### Defined in -[packages/form-core/src/FieldApi.ts:707](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L707) +[packages/form-core/src/FieldApi.ts:666](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L666) *** @@ -488,7 +492,7 @@ Sets the field value and run the `change` validator. #### Defined in -[packages/form-core/src/FieldApi.ts:672](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L672) +[packages/form-core/src/FieldApi.ts:650](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L650) *** @@ -523,7 +527,7 @@ Swaps the values at the specified indices. #### Defined in -[packages/form-core/src/FieldApi.ts:750](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L750) +[packages/form-core/src/FieldApi.ts:709](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L709) *** @@ -547,7 +551,7 @@ Updates the field instance with new options. #### Defined in -[packages/form-core/src/FieldApi.ts:628](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L628) +[packages/form-core/src/FieldApi.ts:606](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L606) *** @@ -571,4 +575,4 @@ Validates the field value. #### Defined in -[packages/form-core/src/FieldApi.ts:1030](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1030) +[packages/form-core/src/FieldApi.ts:989](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L989) diff --git a/docs/reference/classes/formapi.md b/docs/reference/classes/formapi.md index b83a304bc..d9b818804 100644 --- a/docs/reference/classes/formapi.md +++ b/docs/reference/classes/formapi.md @@ -39,10 +39,22 @@ Constructs a new `FormApi` instance with the given form options. #### Defined in -[packages/form-core/src/FormApi.ts:395](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L395) +[packages/form-core/src/FormApi.ts:389](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L389) ## Properties +### baseStore + +```ts +baseStore: Store, (cb) => BaseFormState>; +``` + +#### Defined in + +[packages/form-core/src/FormApi.ts:368](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L368) + +*** + ### fieldInfo ```ts @@ -53,7 +65,19 @@ A record of field information for each field in the form. #### Defined in -[packages/form-core/src/FormApi.ts:384](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L384) +[packages/form-core/src/FormApi.ts:374](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L374) + +*** + +### fieldMetaDerived + +```ts +fieldMetaDerived: Derived ? PrefixTupleAccessor, AllowedIndexes, never>, []> : TFormData extends any[] ? PrefixArrayAccessor, [any]> : TFormData extends Date ? never : TFormData extends object ? PrefixObjectAccessor, []> : TFormData extends string | number | bigint | boolean ? "" : never, FieldMeta>, readonly any[]>; +``` + +#### Defined in + +[packages/form-core/src/FormApi.ts:369](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L369) *** @@ -67,39 +91,37 @@ The options for the form. #### Defined in -[packages/form-core/src/FormApi.ts:368](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L368) +[packages/form-core/src/FormApi.ts:367](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L367) *** -### state +### store ```ts -state: FormState; +store: Derived, readonly any[]>; ``` -The current state of the form. - -**Note:** -Do not use `state` directly, as it is not reactive. -Please use useStore(form.store) utility to subscribe to state - #### Defined in -[packages/form-core/src/FormApi.ts:380](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L380) +[packages/form-core/src/FormApi.ts:370](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L370) -*** +## Accessors -### store +### state + +#### Get Signature ```ts -store: Store, (cb) => FormState>; +get state(): FormState ``` -A [TanStack Store instance](https://tanstack.com/store/latest/docs/reference/Store) that keeps track of the form's state. +##### Returns + +[`FormState`](../type-aliases/formstate.md)\<`TFormData`\> #### Defined in -[packages/form-core/src/FormApi.ts:372](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L372) +[packages/form-core/src/FormApi.ts:377](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L377) ## Methods @@ -125,7 +147,7 @@ deleteField(field): void #### Defined in -[packages/form-core/src/FormApi.ts:1109](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1109) +[packages/form-core/src/FormApi.ts:1182](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1182) *** @@ -153,7 +175,7 @@ Gets the field info of the specified field. #### Defined in -[packages/form-core/src/FormApi.ts:1021](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1021) +[packages/form-core/src/FormApi.ts:1091](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1091) *** @@ -181,7 +203,7 @@ Gets the metadata of the specified field. #### Defined in -[packages/form-core/src/FormApi.ts:1012](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1012) +[packages/form-core/src/FormApi.ts:1082](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1082) *** @@ -209,7 +231,7 @@ Gets the value of the specified field. #### Defined in -[packages/form-core/src/FormApi.ts:1005](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1005) +[packages/form-core/src/FormApi.ts:1075](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1075) *** @@ -227,7 +249,7 @@ Handles the form submission, performs validation, and calls the appropriate onSu #### Defined in -[packages/form-core/src/FormApi.ts:946](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L946) +[packages/form-core/src/FormApi.ts:1016](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1016) *** @@ -271,23 +293,27 @@ Inserts a value into an array field at the specified index, shifting the subsequ #### Defined in -[packages/form-core/src/FormApi.ts:1141](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1141) +[packages/form-core/src/FormApi.ts:1214](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1214) *** ### mount() ```ts -mount(): void +mount(): () => void ``` #### Returns +`Function` + +##### Returns + `void` #### Defined in -[packages/form-core/src/FormApi.ts:532](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L532) +[packages/form-core/src/FormApi.ts:595](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L595) *** @@ -331,7 +357,7 @@ Moves the value at the first specified index to the second specified index withi #### Defined in -[packages/form-core/src/FormApi.ts:1259](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1259) +[packages/form-core/src/FormApi.ts:1332](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1332) *** @@ -370,7 +396,7 @@ Pushes a value into an array field. #### Defined in -[packages/form-core/src/FormApi.ts:1123](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1123) +[packages/form-core/src/FormApi.ts:1196](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1196) *** @@ -409,7 +435,7 @@ Removes a value from an array field at the specified index. #### Defined in -[packages/form-core/src/FormApi.ts:1194](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1194) +[packages/form-core/src/FormApi.ts:1267](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1267) *** @@ -453,7 +479,7 @@ Replaces a value into an array field at the specified index. #### Defined in -[packages/form-core/src/FormApi.ts:1168](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1168) +[packages/form-core/src/FormApi.ts:1241](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1241) *** @@ -488,7 +514,7 @@ Optional options to control the reset behavior. #### Defined in -[packages/form-core/src/FormApi.ts:586](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L586) +[packages/form-core/src/FormApi.ts:656](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L656) *** @@ -514,7 +540,7 @@ resetFieldMeta(fieldMeta): Record #### Defined in -[packages/form-core/src/FormApi.ts:1055](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1055) +[packages/form-core/src/FormApi.ts:1128](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1128) *** @@ -538,7 +564,7 @@ Updates the form's errorMap #### Defined in -[packages/form-core/src/FormApi.ts:1283](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1283) +[packages/form-core/src/FormApi.ts:1356](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1356) *** @@ -570,7 +596,7 @@ Updates the metadata of the specified field. #### Defined in -[packages/form-core/src/FormApi.ts:1040](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1040) +[packages/form-core/src/FormApi.ts:1110](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1110) *** @@ -609,7 +635,7 @@ Sets the value of the specified field and optionally updates the touched state. #### Defined in -[packages/form-core/src/FormApi.ts:1079](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1079) +[packages/form-core/src/FormApi.ts:1152](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1152) *** @@ -653,7 +679,7 @@ Swaps the values at the specified indices within an array field. #### Defined in -[packages/form-core/src/FormApi.ts:1233](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1233) +[packages/form-core/src/FormApi.ts:1306](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L1306) *** @@ -677,7 +703,7 @@ Updates the form options and form state. #### Defined in -[packages/form-core/src/FormApi.ts:542](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L542) +[packages/form-core/src/FormApi.ts:612](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L612) *** @@ -701,7 +727,7 @@ Validates form and all fields in using the correct handlers for a given validati #### Defined in -[packages/form-core/src/FormApi.ts:612](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L612) +[packages/form-core/src/FormApi.ts:682](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L682) *** @@ -740,7 +766,7 @@ Validates the children of a specified array in the form starting from a given in #### Defined in -[packages/form-core/src/FormApi.ts:640](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L640) +[packages/form-core/src/FormApi.ts:710](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L710) *** @@ -772,4 +798,4 @@ Validates a specified field in the form using the correct handlers for a given v #### Defined in -[packages/form-core/src/FormApi.ts:679](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L679) +[packages/form-core/src/FormApi.ts:749](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L749) diff --git a/docs/reference/index.md b/docs/reference/index.md index 1d661151c..6221bdca0 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -21,10 +21,14 @@ title: "@tanstack/form-core" ## Type Aliases +- [BaseFormState](type-aliases/baseformstate.md) - [DeepKeys](type-aliases/deepkeys.md) - [DeepValue](type-aliases/deepvalue.md) +- [DerivedFormState](type-aliases/derivedformstate.md) - [FieldInfo](type-aliases/fieldinfo.md) - [FieldMeta](type-aliases/fieldmeta.md) +- [FieldMetaBase](type-aliases/fieldmetabase.md) +- [FieldMetaDerived](type-aliases/fieldmetaderived.md) - [FieldsErrorMapFromValidator](type-aliases/fieldserrormapfromvalidator.md) - [FieldState](type-aliases/fieldstate.md) - [FormState](type-aliases/formstate.md) diff --git a/docs/reference/type-aliases/baseformstate.md b/docs/reference/type-aliases/baseformstate.md new file mode 100644 index 000000000..277b5c0e7 --- /dev/null +++ b/docs/reference/type-aliases/baseformstate.md @@ -0,0 +1,94 @@ +--- +id: BaseFormState +title: BaseFormState +--- + +# Type Alias: BaseFormState\ + +```ts +type BaseFormState: object; +``` + +An object representing the current state of the form. + +## Type Parameters + +• **TFormData** + +## Type declaration + +### errorMap + +```ts +errorMap: FormValidationErrorMap; +``` + +The error map for the form itself. + +### fieldMetaBase + +```ts +fieldMetaBase: Record, FieldMetaBase>; +``` + +A record of field metadata for each field in the form, not including the derived properties, like `errors` and such + +### isSubmitted + +```ts +isSubmitted: boolean; +``` + +A boolean indicating if the form has been submitted. + +### isSubmitting + +```ts +isSubmitting: boolean; +``` + +A boolean indicating if the form is currently in the process of being submitted after `handleSubmit` is called. + +Goes back to `false` when submission completes for one of the following reasons: +- the validation step returned errors. +- the `onSubmit` function has completed. + +Note: if you're running async operations in your `onSubmit` function make sure to await them to ensure `isSubmitting` is set to `false` only when the async operation completes. + +This is useful for displaying loading indicators or disabling form inputs during submission. + +### isValidating + +```ts +isValidating: boolean; +``` + +A boolean indicating if the form or any of its fields are currently validating. + +### submissionAttempts + +```ts +submissionAttempts: number; +``` + +A counter for tracking the number of submission attempts. + +### validationMetaMap + +```ts +validationMetaMap: Record; +``` + +An internal mechanism used for keeping track of validation logic in a form. + +### values + +```ts +values: TFormData; +``` + +The current values of the form fields. + +## Defined in + +[packages/form-core/src/FormApi.ts:228](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L228) diff --git a/docs/reference/type-aliases/derivedformstate.md b/docs/reference/type-aliases/derivedformstate.md new file mode 100644 index 000000000..59a7579b7 --- /dev/null +++ b/docs/reference/type-aliases/derivedformstate.md @@ -0,0 +1,116 @@ +--- +id: DerivedFormState +title: DerivedFormState +--- + +# Type Alias: DerivedFormState\ + +```ts +type DerivedFormState: object; +``` + +## Type Parameters + +• **TFormData** + +## Type declaration + +### canSubmit + +```ts +canSubmit: boolean; +``` + +A boolean indicating if the form can be submitted based on its current state. + +### errors + +```ts +errors: ValidationError[]; +``` + +The error array for the form itself. + +### fieldMeta + +```ts +fieldMeta: Record, FieldMeta>; +``` + +A record of field metadata for each field in the form. + +### isBlurred + +```ts +isBlurred: boolean; +``` + +A boolean indicating if any of the form fields have been blurred. + +### isDirty + +```ts +isDirty: boolean; +``` + +A boolean indicating if any of the form's fields' values have been modified by the user. `True` if the user have modified at least one of the fields. Opposite of `isPristine`. + +### isFieldsValid + +```ts +isFieldsValid: boolean; +``` + +A boolean indicating if all the form fields are valid. + +### isFieldsValidating + +```ts +isFieldsValidating: boolean; +``` + +A boolean indicating if any of the form fields are currently validating. + +### isFormValid + +```ts +isFormValid: boolean; +``` + +A boolean indicating if the form is valid. + +### isFormValidating + +```ts +isFormValidating: boolean; +``` + +A boolean indicating if the form is currently validating. + +### isPristine + +```ts +isPristine: boolean; +``` + +A boolean indicating if none of the form's fields' values have been modified by the user. `True` if the user have not modified any of the fields. Opposite of `isDirty`. + +### isTouched + +```ts +isTouched: boolean; +``` + +A boolean indicating if any of the form fields have been touched. + +### isValid + +```ts +isValid: boolean; +``` + +A boolean indicating if the form and all its fields are valid. + +## Defined in + +[packages/form-core/src/FormApi.ts:272](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L272) diff --git a/docs/reference/type-aliases/fieldmeta.md b/docs/reference/type-aliases/fieldmeta.md index a86ff54dc..2984539a8 100644 --- a/docs/reference/type-aliases/fieldmeta.md +++ b/docs/reference/type-aliases/fieldmeta.md @@ -6,69 +6,11 @@ title: FieldMeta # Type Alias: FieldMeta ```ts -type FieldMeta: object; +type FieldMeta: FieldMetaBase & FieldMetaDerived; ``` An object type representing the metadata of a field in a form. -## Type declaration - -### errorMap - -```ts -errorMap: ValidationErrorMap; -``` - -A map of errors related to the field value. - -### errors - -```ts -errors: ValidationError[]; -``` - -An array of errors related to the field value. - -### isBlurred - -```ts -isBlurred: boolean; -``` - -A flag indicating whether the field has been blurred. - -### isDirty - -```ts -isDirty: boolean; -``` - -A flag that is `true` if the field's value has been modified by the user. Opposite of `isPristine`. - -### isPristine - -```ts -isPristine: boolean; -``` - -A flag that is `true` if the field's value has not been modified by the user. Opposite of `isDirty`. - -### isTouched - -```ts -isTouched: boolean; -``` - -A flag indicating whether the field has been touched. - -### isValidating - -```ts -isValidating: boolean; -``` - -A flag indicating whether the field is currently being validated. - ## Defined in -[packages/form-core/src/FieldApi.ts:368](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L368) +[packages/form-core/src/FieldApi.ts:402](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L402) diff --git a/docs/reference/type-aliases/fieldmetabase.md b/docs/reference/type-aliases/fieldmetabase.md new file mode 100644 index 000000000..763e9fca8 --- /dev/null +++ b/docs/reference/type-aliases/fieldmetabase.md @@ -0,0 +1,56 @@ +--- +id: FieldMetaBase +title: FieldMetaBase +--- + +# Type Alias: FieldMetaBase + +```ts +type FieldMetaBase: object; +``` + +## Type declaration + +### errorMap + +```ts +errorMap: ValidationErrorMap; +``` + +A map of errors related to the field value. + +### isBlurred + +```ts +isBlurred: boolean; +``` + +A flag indicating whether the field has been blurred. + +### isDirty + +```ts +isDirty: boolean; +``` + +A flag that is `true` if the field's value has been modified by the user. Opposite of `isPristine`. + +### isTouched + +```ts +isTouched: boolean; +``` + +A flag indicating whether the field has been touched. + +### isValidating + +```ts +isValidating: boolean; +``` + +A flag indicating whether the field is currently being validated. + +## Defined in + +[packages/form-core/src/FieldApi.ts:365](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L365) diff --git a/docs/reference/type-aliases/fieldmetaderived.md b/docs/reference/type-aliases/fieldmetaderived.md new file mode 100644 index 000000000..54edb3bdb --- /dev/null +++ b/docs/reference/type-aliases/fieldmetaderived.md @@ -0,0 +1,32 @@ +--- +id: FieldMetaDerived +title: FieldMetaDerived +--- + +# Type Alias: FieldMetaDerived + +```ts +type FieldMetaDerived: object; +``` + +## Type declaration + +### errors + +```ts +errors: ValidationError[]; +``` + +An array of errors related to the field value. + +### isPristine + +```ts +isPristine: boolean; +``` + +A flag that is `true` if the field's value has not been modified by the user. Opposite of `isDirty`. + +## Defined in + +[packages/form-core/src/FieldApi.ts:388](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L388) diff --git a/docs/reference/type-aliases/fieldstate.md b/docs/reference/type-aliases/fieldstate.md index ffdc2fb3e..3054b5c63 100644 --- a/docs/reference/type-aliases/fieldstate.md +++ b/docs/reference/type-aliases/fieldstate.md @@ -35,4 +35,4 @@ The current value of the field. ## Defined in -[packages/form-core/src/FieldApi.ts:402](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L402) +[packages/form-core/src/FieldApi.ts:407](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L407) diff --git a/docs/reference/type-aliases/formstate.md b/docs/reference/type-aliases/formstate.md index d4c96f95f..69f519257 100644 --- a/docs/reference/type-aliases/formstate.md +++ b/docs/reference/type-aliases/formstate.md @@ -6,177 +6,13 @@ title: FormState # Type Alias: FormState\ ```ts -type FormState: object; +type FormState: BaseFormState & DerivedFormState; ``` -An object representing the current state of the form. - ## Type Parameters • **TFormData** -## Type declaration - -### canSubmit - -```ts -canSubmit: boolean; -``` - -A boolean indicating if the form can be submitted based on its current state. - -### errorMap - -```ts -errorMap: FormValidationErrorMap; -``` - -The error map for the form itself. - -### errors - -```ts -errors: ValidationError[]; -``` - -The error array for the form itself. - -### fieldMeta - -```ts -fieldMeta: Record, FieldMeta>; -``` - -A record of field metadata for each field in the form. - -### isBlurred - -```ts -isBlurred: boolean; -``` - -A boolean indicating if any of the form fields have been blurred. - -### isDirty - -```ts -isDirty: boolean; -``` - -A boolean indicating if any of the form's fields' values have been modified by the user. `True` if the user have modified at least one of the fields. Opposite of `isPristine`. - -### isFieldsValid - -```ts -isFieldsValid: boolean; -``` - -A boolean indicating if all the form fields are valid. - -### isFieldsValidating - -```ts -isFieldsValidating: boolean; -``` - -A boolean indicating if any of the form fields are currently validating. - -### isFormValid - -```ts -isFormValid: boolean; -``` - -A boolean indicating if the form is valid. - -### isFormValidating - -```ts -isFormValidating: boolean; -``` - -A boolean indicating if the form is currently validating. - -### isPristine - -```ts -isPristine: boolean; -``` - -A boolean indicating if none of the form's fields' values have been modified by the user. `True` if the user have not modified any of the fields. Opposite of `isDirty`. - -### isSubmitted - -```ts -isSubmitted: boolean; -``` - -A boolean indicating if the form has been submitted. - -### isSubmitting - -```ts -isSubmitting: boolean; -``` - -A boolean indicating if the form is currently in the process of being submitted after `handleSubmit` is called. - -Goes back to `false` when submission completes for one of the following reasons: -- the validation step returned errors. -- the `onSubmit` function has completed. - -Note: if you're running async operations in your `onSubmit` function make sure to await them to ensure `isSubmitting` is set to `false` only when the async operation completes. - -This is useful for displaying loading indicators or disabling form inputs during submission. - -### isTouched - -```ts -isTouched: boolean; -``` - -A boolean indicating if any of the form fields have been touched. - -### isValid - -```ts -isValid: boolean; -``` - -A boolean indicating if the form and all its fields are valid. - -### isValidating - -```ts -isValidating: boolean; -``` - -A boolean indicating if the form or any of its fields are currently validating. - -### submissionAttempts - -```ts -submissionAttempts: number; -``` - -A counter for tracking the number of submission attempts. - -### validationMetaMap - -```ts -validationMetaMap: Record; -``` - -An internal mechanism used for keeping track of validation logic in a form. - -### values - -```ts -values: TFormData; -``` - -The current values of the form fields. - ## Defined in -[packages/form-core/src/FormApi.ts:228](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L228) +[packages/form-core/src/FormApi.ts:323](https://github.com/TanStack/form/blob/main/packages/form-core/src/FormApi.ts#L323) diff --git a/examples/react/next-server-actions/package.json b/examples/react/next-server-actions/package.json index b23d2a0d8..3db935243 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": "^0.40.4", - "@tanstack/react-store": "^0.6.1", + "@tanstack/react-store": "^0.7.0", "next": "15.0.3", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/examples/react/remix/package.json b/examples/react/remix/package.json index 9351a52c3..5f0f03729 100644 --- a/examples/react/remix/package.json +++ b/examples/react/remix/package.json @@ -12,7 +12,7 @@ "@remix-run/react": "^2.15.0", "@remix-run/serve": "^2.15.0", "@tanstack/react-form": "^0.40.4", - "@tanstack/react-store": "^0.6.1", + "@tanstack/react-store": "^0.7.0", "isbot": "^5.1.17", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/examples/react/tanstack-start/package.json b/examples/react/tanstack-start/package.json index 2040cbcfb..7b158b63d 100644 --- a/examples/react/tanstack-start/package.json +++ b/examples/react/tanstack-start/package.json @@ -12,7 +12,7 @@ "@tanstack/form-core": "^0.40.4", "@tanstack/react-form": "^0.40.4", "@tanstack/react-router": "^1.81.1", - "@tanstack/react-store": "^0.6.1", + "@tanstack/react-store": "^0.7.0", "@tanstack/start": "^1.81.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/packages/angular-form/package.json b/packages/angular-form/package.json index 2343c5076..3203a1b66 100644 --- a/packages/angular-form/package.json +++ b/packages/angular-form/package.json @@ -42,7 +42,7 @@ "src" ], "dependencies": { - "@tanstack/angular-store": "^0.6.0", + "@tanstack/angular-store": "^0.7.0", "@tanstack/form-core": "workspace:*", "tslib": "^2.8.1" }, diff --git a/packages/form-core/package.json b/packages/form-core/package.json index 55e8280d9..a5740d052 100644 --- a/packages/form-core/package.json +++ b/packages/form-core/package.json @@ -52,7 +52,7 @@ "src" ], "dependencies": { - "@tanstack/store": "^0.6.0" + "@tanstack/store": "^0.7.0" }, "devDependencies": { "arktype": "2.0.0-rc.23", diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 70b5e17cf..810869b7f 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -1,4 +1,4 @@ -import { Store } from '@tanstack/store' +import { Derived, batch } from '@tanstack/store' import { isStandardSchemaValidator, standardSchemaValidator, @@ -362,10 +362,7 @@ export interface FieldApiOptions< form: FormApi } -/** - * An object type representing the metadata of a field in a form. - */ -export type FieldMeta = { +export type FieldMetaBase = { /** * A flag indicating whether the field has been touched. */ @@ -374,18 +371,10 @@ export type FieldMeta = { * A flag indicating whether the field has been blurred. */ isBlurred: boolean - /** - * A flag that is `true` if the field's value has not been modified by the user. Opposite of `isDirty`. - */ - isPristine: boolean /** * A flag that is `true` if the field's value has been modified by the user. Opposite of `isPristine`. */ isDirty: boolean - /** - * An array of errors related to the field value. - */ - errors: ValidationError[] /** * A map of errors related to the field value. */ @@ -396,6 +385,22 @@ export type FieldMeta = { isValidating: boolean } +export type FieldMetaDerived = { + /** + * An array of errors related to the field value. + */ + errors: ValidationError[] + /** + * A flag that is `true` if the field's value has not been modified by the user. Opposite of `isDirty`. + */ + isPristine: boolean +} + +/** + * An object type representing the metadata of a field in a form. + */ +export type FieldMeta = FieldMetaBase & FieldMetaDerived + /** * An object type representing the state of a field. */ @@ -457,15 +462,13 @@ export class FieldApi< /** * The field state store. */ - store!: Store> + store!: Derived> /** * The current field state. */ - state!: FieldState - /** - * @private - */ - prevState!: FieldState + get state() { + return this.store.state + } timeoutIds: Record | null> /** @@ -489,11 +492,11 @@ export class FieldApi< }) } - this.store = new Store>( - { - value: this.getValue(), - - meta: this._getMeta() ?? { + this.store = new Derived({ + deps: [this.form.store], + fn: () => { + const value = this.form.getFieldValue(this.name) + const meta = this.form.getFieldMeta(this.name) ?? { isValidating: false, isTouched: false, isBlurred: false, @@ -502,26 +505,15 @@ export class FieldApi< errors: [], errorMap: {}, ...opts.defaultMeta, - }, - }, - { - onUpdate: () => { - const state = this.store.state - - state.meta.errors = Object.values(state.meta.errorMap).filter( - (val: unknown) => val !== undefined, - ) - - state.meta.isPristine = !state.meta.isDirty + } - this.prevState = state - this.state = state - }, + return { + value, + meta, + } as FieldState }, - ) + }) - this.state = this.store.state - this.prevState = this.state this.options = opts as never } @@ -573,22 +565,10 @@ export class FieldApi< * Mounts the field instance to the form. */ mount = () => { + const cleanup = this.store.mount() + const info = this.getInfo() info.instance = this as never - const unsubscribe = this.form.store.subscribe(() => { - this.store.batch(() => { - const nextValue = this.getValue() - const nextMeta = this.getMeta() - - if (nextValue !== this.state.value) { - this.store.setState((prev) => ({ ...prev, value: nextValue })) - } - - if (nextMeta !== this.state.meta) { - this.store.setState((prev) => ({ ...prev, meta: nextMeta })) - } - }) - }) this.update(this.options as never) const { onMount } = this.options.validators || {} @@ -617,9 +597,7 @@ export class FieldApi< fieldApi: this, }) - return () => { - unsubscribe() - } + return cleanup } /** @@ -651,7 +629,7 @@ export class FieldApi< } // Default Meta - if (this._getMeta() === undefined) { + if (this.form.getFieldMeta(this.name) === undefined) { this.setMeta(this.state.meta) } @@ -680,26 +658,7 @@ export class FieldApi< this.validate('change') } - /** - * @private - */ - _getMeta = () => this.form.getFieldMeta(this.name) - - /** - * Gets the current field metadata. - */ - getMeta = () => - this._getMeta() ?? - ({ - isValidating: false, - isTouched: false, - isBlurred: false, - isDirty: false, - isPristine: true, - errors: [], - errorMap: {}, - ...this.options.defaultMeta, - } as FieldMeta) + getMeta = () => this.store.state.meta /** * Sets the field metadata. @@ -808,7 +767,7 @@ export class FieldApi< // Needs type cast as eslint errantly believes this is always falsy let hasErrored = false as boolean - this.form.store.batch(() => { + batch(() => { const validateFieldFn = ( field: FieldApi, validateObj: SyncValidator, @@ -827,7 +786,7 @@ export class FieldApi< field.runValidator({ validate: validateObj.validate, value: { - value: field.getValue(), + value: field.store.state.value, validationSource: 'field', fieldApi: field, }, @@ -958,7 +917,7 @@ export class FieldApi< await this.runValidator({ validate: validateObj.validate, value: { - value: field.getValue(), + value: field.store.state.value, fieldApi: field, signal: controller.signal, validationSource: 'field', diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index af0190c28..93fec3a05 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1,4 +1,4 @@ -import { Store } from '@tanstack/store' +import { Derived, Store, batch } from '@tanstack/store' import { deleteBy, functionalUpdate, @@ -13,7 +13,7 @@ import { standardSchemaValidator, } from './standardSchemaValidator' import type { StandardSchemaV1 } from './standardSchemaValidator' -import type { FieldApi, FieldMeta } from './FieldApi' +import type { FieldApi, FieldMeta, FieldMetaBase } from './FieldApi' import type { FormValidationError, FormValidationErrorMap, @@ -225,23 +225,11 @@ export type FieldInfo< /** * An object representing the current state of the form. */ -export type FormState = { +export type BaseFormState = { /** * The current values of the form fields. */ values: TFormData - /** - * A boolean indicating if the form is currently validating. - */ - isFormValidating: boolean - /** - * A boolean indicating if the form is valid. - */ - isFormValid: boolean - /** - * The error array for the form itself. - */ - errors: ValidationError[] /** * The error map for the form itself. */ @@ -251,17 +239,9 @@ export type FormState = { */ validationMetaMap: Record /** - * A record of field metadata for each field in the form. - */ - fieldMeta: Record, FieldMeta> - /** - * A boolean indicating if any of the form fields are currently validating. - */ - isFieldsValidating: boolean - /** - * A boolean indicating if all the form fields are valid. + * A record of field metadata for each field in the form, not including the derived properties, like `errors` and such */ - isFieldsValid: boolean + fieldMetaBase: Record, FieldMetaBase> /** * A boolean indicating if the form is currently in the process of being submitted after `handleSubmit` is called. * @@ -275,6 +255,41 @@ export type FormState = { * */ isSubmitting: boolean + /** + * A boolean indicating if the form has been submitted. + */ + isSubmitted: boolean + /** + * A boolean indicating if the form or any of its fields are currently validating. + */ + isValidating: boolean + /** + * A counter for tracking the number of submission attempts. + */ + submissionAttempts: number +} + +export type DerivedFormState = { + /** + * A boolean indicating if the form is currently validating. + */ + isFormValidating: boolean + /** + * A boolean indicating if the form is valid. + */ + isFormValid: boolean + /** + * The error array for the form itself. + */ + errors: ValidationError[] + /** + * A boolean indicating if any of the form fields are currently validating. + */ + isFieldsValidating: boolean + /** + * A boolean indicating if all the form fields are valid. + */ + isFieldsValid: boolean /** * A boolean indicating if any of the form fields have been touched. */ @@ -291,14 +306,6 @@ export type FormState = { * A boolean indicating if none of the form's fields' values have been modified by the user. `True` if the user have not modified any of the fields. Opposite of `isDirty`. */ isPristine: boolean - /** - * A boolean indicating if the form has been submitted. - */ - isSubmitted: boolean - /** - * A boolean indicating if the form or any of its fields are currently validating. - */ - isValidating: boolean /** * A boolean indicating if the form and all its fields are valid. */ @@ -308,31 +315,23 @@ export type FormState = { */ canSubmit: boolean /** - * A counter for tracking the number of submission attempts. + * A record of field metadata for each field in the form. */ - submissionAttempts: number + fieldMeta: Record, FieldMeta> } +export type FormState = BaseFormState & + DerivedFormState + function getDefaultFormState( defaultState: Partial>, -): FormState { +): BaseFormState { return { values: defaultState.values ?? ({} as never), - errors: defaultState.errors ?? [], errorMap: defaultState.errorMap ?? {}, - fieldMeta: defaultState.fieldMeta ?? ({} as never), - canSubmit: defaultState.canSubmit ?? true, - isFieldsValid: defaultState.isFieldsValid ?? false, - isFieldsValidating: defaultState.isFieldsValidating ?? false, - isFormValid: defaultState.isFormValid ?? false, - isFormValidating: defaultState.isFormValidating ?? false, + fieldMetaBase: defaultState.fieldMetaBase ?? ({} as never), isSubmitted: defaultState.isSubmitted ?? false, isSubmitting: defaultState.isSubmitting ?? false, - isTouched: defaultState.isTouched ?? false, - isBlurred: defaultState.isBlurred ?? false, - isPristine: defaultState.isPristine ?? true, - isDirty: defaultState.isDirty ?? false, - isValid: defaultState.isValid ?? false, isValidating: defaultState.isValidating ?? false, submissionAttempts: defaultState.submissionAttempts ?? 0, validationMetaMap: defaultState.validationMetaMap ?? { @@ -366,24 +365,19 @@ export class FormApi< * The options for the form. */ options: FormOptions = {} - /** - * A [TanStack Store instance](https://tanstack.com/store/latest/docs/reference/Store) that keeps track of the form's state. - */ - store!: Store> - /** - * The current state of the form. - * - * **Note:** - * Do not use `state` directly, as it is not reactive. - * Please use useStore(form.store) utility to subscribe to state - */ - state!: FormState + baseStore!: Store> + fieldMetaDerived!: Derived, FieldMeta>> + store!: Derived> /** * A record of field information for each field in the form. */ fieldInfo: Record, FieldInfo> = {} as any + get state() { + return this.store.state + } + /** * @private */ @@ -393,103 +387,172 @@ export class FormApi< * Constructs a new `FormApi` instance with the given form options. */ constructor(opts?: FormOptions) { - this.store = new Store>( + this.baseStore = new Store( getDefaultFormState({ ...(opts?.defaultState as any), values: opts?.defaultValues ?? opts?.defaultState?.values, isFormValid: true, }), - { - onUpdate: () => { - let { state } = this.store - // Computed state - const fieldMetaValues = Object.values(state.fieldMeta) as ( - | FieldMeta - | undefined - )[] + ) - const isFieldsValidating = fieldMetaValues.some( - (field) => field?.isValidating, - ) + this.fieldMetaDerived = new Derived({ + deps: [this.baseStore], + fn: ({ prevDepVals, currDepVals, prevVal: _prevVal }) => { + const prevVal = _prevVal as + | Record, FieldMeta> + | undefined + const prevBaseStore = prevDepVals?.[0] + const currBaseStore = currDepVals[0] + + const fieldMeta = {} as FormState['fieldMeta'] + for (const fieldName of Object.keys( + currBaseStore.fieldMetaBase, + ) as Array) { + const currBaseVal = currBaseStore.fieldMetaBase[ + fieldName as never + ] as FieldMetaBase + + const prevBaseVal = prevBaseStore?.fieldMetaBase[ + fieldName as never + ] as FieldMetaBase | undefined + + let fieldErrors = + prevVal?.[fieldName as never as keyof typeof prevVal]?.errors + if (!prevBaseVal || currBaseVal.errorMap !== prevBaseVal.errorMap) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + fieldErrors = Object.values(currBaseVal.errorMap ?? {}).filter( + (val: unknown) => val !== undefined, + ) + } - const isFieldsValid = !fieldMetaValues.some( - (field) => - field?.errorMap && - isNonEmptyArray(Object.values(field.errorMap).filter(Boolean)), - ) + // As a primitive, we don't need to aggressively persist the same referencial value for performance reasons + const isFieldPristine = !currBaseVal.isDirty - const isTouched = fieldMetaValues.some((field) => field?.isTouched) - const isBlurred = fieldMetaValues.some((field) => field?.isBlurred) + fieldMeta[fieldName] = { + ...currBaseVal, + errors: fieldErrors, + isPristine: isFieldPristine, + } as FieldMeta + } + + return fieldMeta + }, + }) + this.store = new Derived({ + deps: [this.baseStore, this.fieldMetaDerived], + fn: ({ prevDepVals, currDepVals, prevVal: _prevVal }) => { + const prevVal = _prevVal as FormState | undefined + const prevBaseStore = prevDepVals?.[0] + const currBaseStore = currDepVals[0] + + // Computed state + const fieldMetaValues = Object.values(currBaseStore.fieldMetaBase) as ( + | FieldMeta + | undefined + )[] + + const isFieldsValidating = fieldMetaValues.some( + (field) => field?.isValidating, + ) + + const isFieldsValid = !fieldMetaValues.some( + (field) => + field?.errorMap && + isNonEmptyArray(Object.values(field.errorMap).filter(Boolean)), + ) + + const isTouched = fieldMetaValues.some((field) => field?.isTouched) + const isBlurred = fieldMetaValues.some((field) => field?.isBlurred) + + const shouldInvalidateOnMount = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (isTouched && state?.errorMap?.onMount) { - state.errorMap.onMount = undefined - } + 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( + const hasOnMountError = Boolean( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + currBaseStore.errorMap?.onMount || // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - state.errorMap?.onMount || - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - fieldMetaValues.some((f) => f?.errorMap?.onMount), - ) + fieldMetaValues.some((f) => f?.errorMap?.onMount), + ) - const isValidating = isFieldsValidating || state.isFormValidating - state.errors = Object.values(state.errorMap).reduce((prev, curr) => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (curr === undefined) return prev - if (typeof curr === 'string') { - prev.push(curr) - return prev - } else if (curr && isFormValidationError(curr)) { - prev.push(curr.form) + 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( + (prev, curr) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (curr === undefined) return prev + if (typeof curr === 'string') { + prev.push(curr) + return prev + } else if (curr && isFormValidationError(curr)) { + prev.push(curr.form) + return prev + } return prev - } - return prev - }, [] as ValidationError[]) - const isFormValid = state.errors.length === 0 - const isValid = isFieldsValid && isFormValid - const canSubmit = - (state.submissionAttempts === 0 && - !isTouched && - !hasOnMountError) || - (!isValidating && !state.isSubmitting && isValid) - - state = { - ...state, - isFieldsValidating, - isFieldsValid, - isFormValid, - isValid, - canSubmit, - isTouched, - isBlurred, - isPristine, - isDirty, - } + }, + [] as ValidationError[], + ) + } - this.state = state - this.store.state = this.state + const isFormValid = errors.length === 0 + const isValid = isFieldsValid && isFormValid + const canSubmit = + (currBaseStore.submissionAttempts === 0 && + !isTouched && + !hasOnMountError) || + (!isValidating && !currBaseStore.isSubmitting && isValid) + + let errorMap = currBaseStore.errorMap + if (shouldInvalidateOnMount) { + errors = errors.filter( + (err) => err !== currBaseStore.errorMap.onMount, + ) + errorMap = Object.assign(errorMap, { onMount: 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]) + let state = { + ...currBaseStore, + errorMap, + fieldMeta: this.fieldMetaDerived.state, + errors, + isFieldsValidating, + isFieldsValid, + isFormValid, + isValid, + canSubmit, + isTouched, + isBlurred, + isPristine, + isDirty, + } as FormState + + // 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 + } - if (shouldTransform) { - // This mutates the state - this.options.transform?.fn(this) - this.store.state = this.state - this.prevTransformArray = transformArray - } - }, + return state }, - ) - - this.state = this.store.state + }) this.update(opts || {}) } @@ -530,10 +593,17 @@ export class FormApi< } mount = () => { + const cleanupFieldMetaDerived = this.fieldMetaDerived.mount() + const cleanupStoreDerived = this.store.mount() + const cleanup = () => { + cleanupFieldMetaDerived() + cleanupStoreDerived() + } const { onMount } = this.options.validators || {} - if (!onMount) return - + if (!onMount) return cleanup this.validateSync('mount') + + return cleanup } /** @@ -547,7 +617,7 @@ export class FormApi< // Options need to be updated first so that when the store is updated, the state is correct for the derived state this.options = options - this.store.batch(() => { + batch(() => { const shouldUpdateValues = options.defaultValues && options.defaultValues !== oldOptions.defaultValues && @@ -557,7 +627,7 @@ export class FormApi< options.defaultState !== oldOptions.defaultState && !this.state.isTouched - this.store.setState(() => + this.baseStore.setState(() => getDefaultFormState( Object.assign( {}, @@ -585,7 +655,7 @@ export class FormApi< */ reset = (values?: TFormData, opts?: { keepDefaultValues?: boolean }) => { const { fieldMeta: currentFieldMeta } = this.state - const fieldMeta = this.resetFieldMeta(currentFieldMeta) + const fieldMetaBase = this.resetFieldMeta(currentFieldMeta) if (values && !opts?.keepDefaultValues) { this.options = { @@ -594,14 +664,14 @@ export class FormApi< } } - this.store.setState(() => + this.baseStore.setState(() => getDefaultFormState({ ...(this.options.defaultState as any), values: values ?? this.options.defaultValues ?? this.options.defaultState?.values, - fieldMeta, + fieldMetaBase, }), ) } @@ -611,7 +681,7 @@ export class FormApi< */ validateAllFields = async (cause: ValidationCause) => { const fieldValidationPromises: Promise[] = [] as any - this.store.batch(() => { + batch(() => { void ( Object.values(this.fieldInfo) as FieldInfo[] ).forEach((field) => { @@ -661,7 +731,7 @@ export class FormApi< // Validate the fields const fieldValidationPromises: Promise[] = [] as any - this.store.batch(() => { + batch(() => { fieldsToValidate.forEach((nestedField) => { fieldValidationPromises.push( Promise.resolve().then(() => this.validateField(nestedField, cause)), @@ -708,7 +778,7 @@ export class FormApi< const fieldsErrorMap: FieldsErrorMapFromValidator = {} - this.store.batch(() => { + batch(() => { for (const validateObj of validates) { if (!validateObj.validate) continue @@ -750,7 +820,7 @@ export class FormApi< } if (this.state.errorMap[errorMapKey] !== formError) { - this.store.setState((prev) => ({ + this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, @@ -775,7 +845,7 @@ export class FormApi< cause !== 'submit' && !hasErrored ) { - this.store.setState((prev) => ({ + this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, @@ -796,7 +866,7 @@ export class FormApi< const validates = getAsyncValidatorArray(cause, this.options) if (!this.state.isFormValidating) { - this.store.setState((prev) => ({ ...prev, isFormValidating: true })) + this.baseStore.setState((prev) => ({ ...prev, isFormValidating: true })) } /** @@ -876,7 +946,7 @@ export class FormApi< } } } - this.store.setState((prev) => ({ + this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, @@ -913,7 +983,7 @@ export class FormApi< } } - this.store.setState((prev) => ({ + this.baseStore.setState((prev) => ({ ...prev, isFormValidating: false, })) @@ -944,7 +1014,7 @@ export class FormApi< * Handles the form submission, performs validation, and calls the appropriate onSubmit or onInvalidSubmit callbacks. */ handleSubmit = async () => { - this.store.setState((old) => ({ + this.baseStore.setState((old) => ({ ...old, // Submission attempts mark the form as not submitted isSubmitted: false, @@ -955,10 +1025,10 @@ export class FormApi< // Don't let invalid forms submit if (!this.state.canSubmit) return - this.store.setState((d) => ({ ...d, isSubmitting: true })) + this.baseStore.setState((d) => ({ ...d, isSubmitting: true })) const done = () => { - this.store.setState((prev) => ({ ...prev, isSubmitting: false })) + this.baseStore.setState((prev) => ({ ...prev, isSubmitting: false })) } // Validate form and all fields @@ -974,7 +1044,7 @@ export class FormApi< return } - this.store.batch(() => { + batch(() => { void ( Object.values(this.fieldInfo) as FieldInfo[] ).forEach((field) => { @@ -989,8 +1059,8 @@ export class FormApi< // Run the submit code await this.options.onSubmit?.({ value: this.state.values, formApi: this }) - this.store.batch(() => { - this.store.setState((prev) => ({ ...prev, isSubmitted: true })) + batch(() => { + this.baseStore.setState((prev) => ({ ...prev, isSubmitted: true })) done() }) } catch (err) { @@ -1041,12 +1111,15 @@ export class FormApi< field: TField, updater: Updater, ) => { - this.store.setState((prev) => { + this.baseStore.setState((prev) => { return { ...prev, - fieldMeta: { - ...prev.fieldMeta, - [field]: functionalUpdate(updater, prev.fieldMeta[field]), + fieldMetaBase: { + ...prev.fieldMetaBase, + [field]: functionalUpdate( + updater, + prev.fieldMetaBase[field] as never, + ), }, } }) @@ -1083,7 +1156,7 @@ export class FormApi< ) => { const dontUpdateMeta = opts?.dontUpdateMeta ?? false - this.store.batch(() => { + batch(() => { if (!dontUpdateMeta) { this.setFieldMeta(field, (prev) => ({ ...prev, @@ -1097,7 +1170,7 @@ export class FormApi< })) } - this.store.setState((prev) => { + this.baseStore.setState((prev) => { return { ...prev, values: setBy(prev.values, field, updater), @@ -1107,10 +1180,10 @@ export class FormApi< } deleteField = >(field: TField) => { - this.store.setState((prev) => { + this.baseStore.setState((prev) => { const newState = { ...prev } newState.values = deleteBy(newState.values, field) - delete newState.fieldMeta[field] + delete newState.fieldMetaBase[field] return newState }) @@ -1281,7 +1354,7 @@ export class FormApi< * Updates the form's errorMap */ setErrorMap(errorMap: ValidationErrorMap) { - this.store.setState((prev) => ({ + this.baseStore.setState((prev) => ({ ...prev, errorMap: { ...prev.errorMap, diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index 8bb6dd393..157bda920 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -10,11 +10,15 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', }) + field.mount() + expect(field.getValue()).toBe('test') }) @@ -24,6 +28,7 @@ describe('field api', () => { name: 'test', }, }) + form.mount() const field = new FieldApi({ form, @@ -31,16 +36,23 @@ describe('field api', () => { name: 'name', }) + field.mount() + expect(field.getValue()).toBe('other') }) it('should get default meta', () => { const form = new FormApi() + + form.mount() + const field = new FieldApi({ form, name: 'name', }) + field.mount() + expect(field.getMeta()).toEqual({ isTouched: false, isBlurred: false, @@ -54,6 +66,9 @@ describe('field api', () => { it('should allow to set default meta', () => { const form = new FormApi() + + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -65,6 +80,8 @@ describe('field api', () => { }, }) + field.mount() + expect(field.getMeta()).toEqual({ isTouched: true, isBlurred: true, @@ -83,11 +100,15 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', }) + field.mount() + field.setValue('other', { dontUpdateMeta: true, }) @@ -148,11 +169,15 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'names', }) + field.mount() + field.pushValue('other') expect(field.getValue()).toStrictEqual(['one', 'other']) @@ -164,6 +189,7 @@ describe('field api', () => { names: ['test'], }, }) + form.mount() const field = new FieldApi({ @@ -178,6 +204,7 @@ describe('field api', () => { }, }, }) + field.mount() field.pushValue('other') @@ -194,11 +221,15 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'names', }) + field.mount() + field.insertValue(1, 'other') expect(field.getValue()).toStrictEqual(['one', 'other', 'two']) @@ -211,11 +242,15 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'names', }) + field.mount() + field.replaceValue(1, 'other') expect(field.getValue()).toStrictEqual(['one', 'other', 'three']) @@ -228,11 +263,15 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'names', }) + field.mount() + field.replaceValue(10, 'other') expect(field.getValue()).toStrictEqual(['one', 'two', 'three']) @@ -277,11 +316,15 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'names', }) + field.mount() + field.removeValue(1) expect(field.getValue()).toStrictEqual(['one']) @@ -419,7 +462,9 @@ describe('field api', () => { validators: subFieldValidators, }) - ;[form, field, subField1].forEach((f) => f.mount()) + form.mount() + field.mount() + subField1.mount() await form.handleSubmit() @@ -440,11 +485,15 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'names', }) + field.mount() + field.swapValues(0, 1) expect(field.getValue()).toStrictEqual(['two', 'one']) @@ -489,11 +538,15 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'names', }) + field.mount() + field.moveValue(2, 0) expect(field.getValue()).toStrictEqual(['three', 'one', 'two', 'four']) @@ -538,6 +591,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'items.2.quantity', @@ -561,6 +616,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -584,6 +641,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -616,6 +675,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -649,6 +710,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -688,6 +751,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -737,6 +802,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -787,6 +854,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -838,6 +907,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -868,6 +939,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -902,6 +975,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -940,6 +1015,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -977,6 +1054,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -1008,6 +1087,8 @@ describe('field api', () => { }, }) + form.mount() + let triggered!: string const field = new FieldApi({ form, @@ -1056,6 +1137,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -1081,6 +1164,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -1155,6 +1240,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -1191,6 +1278,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -1222,6 +1311,8 @@ describe('field api', () => { } const form = new FormApi
() + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -1241,6 +1332,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'name', @@ -1259,6 +1352,8 @@ describe('field api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'firstName', @@ -1388,6 +1483,8 @@ describe('field api', () => { }, }) + form.mount() + let resolve!: () => void const promise = new Promise((r) => { resolve = r as never @@ -1427,6 +1524,8 @@ describe('field api', () => { }, }) + form.mount() + const passField = new FieldApi({ form, name: 'password', @@ -1469,6 +1568,8 @@ describe('field api', () => { }, }) + form.mount() + const passField = new FieldApi({ form, name: 'password', @@ -1526,6 +1627,8 @@ describe('field api', () => { }, }) + form.mount() + const passField = new FieldApi({ form, name: 'password', @@ -1579,6 +1682,7 @@ describe('field api', () => { name: string } const form = new FormApi() + form.mount() const nameField = new FieldApi({ form, name: 'name', @@ -1594,6 +1698,7 @@ describe('field api', () => { name: string } const form = new FormApi() + form.mount() const nameField = new FieldApi({ form, name: 'name', @@ -1616,6 +1721,7 @@ describe('field api', () => { name: string } const form = new FormApi() + form.mount() const nameField = new FieldApi({ form, name: 'name', @@ -1632,4 +1738,26 @@ describe('field api', () => { 'other validation error', ) }) + + it('should have derived state on first render given defaultMeta', () => { + const form = new FormApi({ + defaultValues: { + name: '', + }, + }) + form.mount() + + const nameField = new FieldApi({ + form, + name: 'name', + defaultMeta: { + errorMap: { + onChange: 'THERE IS AN ERROR', + }, + }, + }) + + nameField.mount() + expect(nameField.getMeta().errors).toEqual(['THERE IS AN ERROR']) + }) }) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index cae8e71c4..1a6570981 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -3,37 +3,6 @@ import { FieldApi, FormApi } from '../src/index' import { sleep } from './utils' describe('form api', () => { - it('should get default form state', () => { - const form = new FormApi() - form.mount() - expect(form.state).toEqual({ - values: {}, - fieldMeta: {}, - canSubmit: true, - isFieldsValid: true, - isFieldsValidating: false, - isFormValid: true, - isFormValidating: false, - isSubmitted: false, - errors: [], - errorMap: {}, - isSubmitting: false, - isTouched: false, - isBlurred: false, - isPristine: true, - isDirty: false, - isValid: true, - isValidating: false, - submissionAttempts: 0, - validationMetaMap: { - onChange: undefined, - onBlur: undefined, - onSubmit: undefined, - onMount: undefined, - }, - }) - }) - it('should get default form state when default values are passed', () => { const form = new FormApi({ defaultValues: { @@ -41,33 +10,10 @@ describe('form api', () => { }, }) form.mount() - expect(form.state).toEqual({ + expect(form.state).toMatchObject({ values: { name: 'test', }, - fieldMeta: {}, - canSubmit: true, - isFieldsValid: true, - errors: [], - errorMap: {}, - isFieldsValidating: false, - isFormValid: true, - isFormValidating: false, - isSubmitted: false, - isSubmitting: false, - isTouched: false, - isBlurred: false, - isPristine: true, - isDirty: false, - isValid: true, - isValidating: false, - submissionAttempts: 0, - validationMetaMap: { - onChange: undefined, - onBlur: undefined, - onSubmit: undefined, - onMount: undefined, - }, }) }) @@ -78,31 +24,8 @@ describe('form api', () => { }, }) form.mount() - expect(form.state).toEqual({ - values: {}, - fieldMeta: {}, - errors: [], - errorMap: {}, - canSubmit: true, - isFieldsValid: true, - isFieldsValidating: false, - isFormValid: true, - isFormValidating: false, - isSubmitted: false, - isSubmitting: false, - isTouched: false, - isBlurred: false, - isPristine: true, - isDirty: false, - isValid: true, - isValidating: false, + expect(form.state).toMatchObject({ submissionAttempts: 30, - validationMetaMap: { - onChange: undefined, - onBlur: undefined, - onSubmit: undefined, - onMount: undefined, - }, }) }) @@ -122,34 +45,11 @@ describe('form api', () => { }, }) - expect(form.state).toEqual({ + expect(form.state).toMatchObject({ values: { name: 'other', }, - errors: [], - errorMap: {}, - fieldMeta: {}, - canSubmit: true, - isFieldsValid: true, - isFieldsValidating: false, - isFormValid: true, - isFormValidating: false, - isSubmitted: false, - isSubmitting: false, - isTouched: false, - isBlurred: false, - isPristine: true, - isDirty: false, - isValid: true, - isValidating: false, submissionAttempts: 300, - validationMetaMap: { - onChange: undefined, - onBlur: undefined, - onSubmit: undefined, - onMount: undefined, - onServer: undefined, - }, }) }) @@ -167,33 +67,12 @@ describe('form api', () => { form.reset() - expect(form.state).toEqual({ + expect(form.state).toMatchObject({ values: { name: 'test', }, - errors: [], - errorMap: {}, fieldMeta: {}, - canSubmit: true, - isFieldsValid: true, - isFieldsValidating: false, - isFormValid: true, - isFormValidating: false, - isSubmitted: false, - isSubmitting: false, - isTouched: false, - isBlurred: false, - isPristine: true, - isDirty: false, - isValid: true, - isValidating: false, submissionAttempts: 0, - validationMetaMap: { - onChange: undefined, - onBlur: undefined, - onSubmit: undefined, - onMount: undefined, - }, }) }) @@ -230,55 +109,12 @@ describe('form api', () => { } form.reset(resetValues) - expect(form.state).toEqual({ + expect(form.state).toMatchObject({ values: { name: 'reset name', age: 40, surname: 'reset surname', }, - errors: [], - errorMap: {}, - fieldMeta: { - name: { - isValidating: false, - isTouched: false, - isBlurred: false, - isDirty: false, - isPristine: true, - errors: [], - errorMap: {}, - }, - age: { - isValidating: false, - isTouched: false, - isBlurred: false, - isDirty: false, - isPristine: true, - errors: [], - errorMap: {}, - }, - }, - canSubmit: true, - isFieldsValid: true, - isFieldsValidating: false, - isFormValid: true, - isFormValidating: false, - isSubmitted: false, - isSubmitting: false, - isTouched: false, - isPristine: true, - isDirty: false, - isValid: true, - isBlurred: false, - isValidating: false, - submissionAttempts: 0, - validationMetaMap: { - onChange: undefined, - onBlur: undefined, - onSubmit: undefined, - onMount: undefined, - onServer: undefined, - }, }) }) @@ -312,71 +148,6 @@ describe('form api', () => { expect(form.state.values).toEqual({ name: 'initial' }) }) - it('should not wipe validators when resetting', () => { - const form = new FormApi({ - defaultValues: { - name: 'test', - }, - }) - - const field = new FieldApi({ - form, - name: 'name', - validators: { - onChange: ({ value }) => (value.length > 0 ? undefined : 'required'), - }, - }) - - form.mount() - - field.mount() - - field.handleChange('') - - expect(form.state.isFieldsValid).toEqual(false) - expect(form.state.canSubmit).toEqual(false) - - form.reset() - - expect(form.state).toEqual({ - values: { name: 'test' }, - errors: [], - errorMap: {}, - fieldMeta: { - name: { - isValidating: false, - isTouched: false, - isBlurred: false, - isDirty: false, - isPristine: true, - errors: [], - errorMap: {}, - }, - }, - canSubmit: true, - isFieldsValid: true, - isFieldsValidating: false, - isFormValid: true, - isFormValidating: false, - isSubmitted: false, - isSubmitting: false, - isTouched: false, - isBlurred: false, - isPristine: true, - isDirty: false, - isValid: true, - isValidating: false, - submissionAttempts: 0, - validationMetaMap: { - onChange: undefined, - onBlur: undefined, - onSubmit: undefined, - onMount: undefined, - onServer: undefined, - }, - }) - }) - it("should get a field's value", () => { const form = new FormApi({ defaultValues: { @@ -474,7 +245,8 @@ describe('form api', () => { }) form.mount() // Since validation runs through the field, a field must be mounted for that array - new FieldApi({ form, name: 'names' }).mount() + const field = new FieldApi({ form, name: 'names' }) + field.mount() form.pushFieldValue('names', 'other') @@ -831,6 +603,8 @@ describe('form api', () => { const form = new FormApi() + form.mount() + const field = new FieldApi({ form, name: 'employees', @@ -858,7 +632,7 @@ describe('form api', () => { } const form = new FormApi() - + form.mount() const field = new FieldApi({ form, name: 'employees', @@ -939,7 +713,7 @@ describe('form api', () => { age: 4, }, }) - + form.mount() form.deleteField('names') expect(form.getFieldValue('age')).toStrictEqual(4) @@ -1384,6 +1158,7 @@ describe('form api', () => { lastName: '', }, }) + form.mount() const field = new FieldApi({ form, @@ -1423,7 +1198,7 @@ describe('form api', () => { person: null, } as { person: { firstName: string; lastName: string } | null }, }) - + form.mount() const field = new FieldApi({ form, name: 'person.firstName', @@ -1463,7 +1238,7 @@ describe('form api', () => { lastName: '', }, }) - + form.mount() const field = new FieldApi({ form, name: 'firstName', @@ -1507,7 +1282,7 @@ describe('form api', () => { onSubmitAsync: formSubmit, }, }) - + form.mount() const field = new FieldApi({ form, name: 'firstName', @@ -1542,7 +1317,7 @@ describe('form api', () => { lastName: '', }, }) - + form.mount() const field = new FieldApi({ form, name: 'firstName', @@ -1579,7 +1354,7 @@ describe('form api', () => { firstName: '', }, }) - + form.mount() const field = new FieldApi({ form, name: 'firstName', @@ -1622,8 +1397,8 @@ describe('form api', () => { }, }) - field.mount() form.mount() + field.mount() await form.validateAllFields('change') expect(field.getMeta().errorMap.onChange).toEqual('first name is required') @@ -1651,8 +1426,8 @@ describe('form api', () => { }, }) - field.mount() form.mount() + field.mount() await form.validateField('firstName', 'change') expect(field.getMeta().errorMap.onChange).toEqual('first name is required') @@ -1671,6 +1446,8 @@ describe('form api', () => { }, }) + form.mount() + const field = new FieldApi({ form, name: 'firstName', @@ -1692,7 +1469,7 @@ describe('form api', () => { value.firstName.length > 0 ? undefined : 'first name is required', }, }) - + form.mount() const field = new FieldApi({ form, name: 'firstName', @@ -1734,7 +1511,7 @@ describe('form api', () => { person: null, } as { person: { firstName: string } | null }, }) - + form.mount() const field = new FieldApi({ form, name: 'person.firstName', @@ -1754,7 +1531,7 @@ describe('form api', () => { person: null, } as { person: { nameInfo: { first: string } | null } | null }, }) - + form.mount() const field = new FieldApi({ form, name: 'person.nameInfo.first', @@ -1774,7 +1551,7 @@ describe('form api', () => { persons: null, } as { persons: Array<{ nameInfo: { first: string } }> | null }, }) - + form.mount() const field = new FieldApi({ form, name: 'persons', @@ -1793,6 +1570,7 @@ describe('form api', () => { name: string } const form = new FormApi() + form.mount() form.setErrorMap({ onChange: "name can't be Josh", }) @@ -1804,6 +1582,7 @@ describe('form api', () => { name: string } const form = new FormApi() + form.mount() form.setErrorMap({ onChange: "name can't be Josh", }) @@ -1820,6 +1599,7 @@ describe('form api', () => { name: string } const form = new FormApi() + form.mount() form.setErrorMap({ onChange: "name can't be Josh", }) @@ -1851,6 +1631,8 @@ describe('form api', () => { }, }) + form.mount() + const firstNameField = new FieldApi({ form, name: 'firstName', @@ -2028,7 +1810,7 @@ describe('form api', () => { }, }, }) - + form.mount() const firstNameField = new FieldApi({ form, name: 'firstName', @@ -2090,7 +1872,7 @@ describe('form api', () => { }, }, }) - + form.mount() const firstNameField = new FieldApi({ form, name: 'firstName', @@ -2159,7 +1941,7 @@ describe('form api', () => { }, }, }) - + form.mount() const field = new FieldApi({ form, name: 'names', @@ -2213,7 +1995,7 @@ describe('form api', () => { }, }, }) - + form.mount() const field = new FieldApi({ form, name: 'employees', @@ -2266,7 +2048,7 @@ describe('form api', () => { }, }, }) - + form.mount() const passField = new FieldApi({ form, name: 'password', @@ -2318,7 +2100,7 @@ describe('form api', () => { }, }, }) - + form.mount() const passField = new FieldApi({ form, name: 'password', diff --git a/packages/react-form/package.json b/packages/react-form/package.json index 6e8baf00c..0f1fb2d2d 100644 --- a/packages/react-form/package.json +++ b/packages/react-form/package.json @@ -84,7 +84,7 @@ "dependencies": { "@remix-run/node": "^2.15.0", "@tanstack/form-core": "workspace:*", - "@tanstack/react-store": "^0.6.1", + "@tanstack/react-store": "^0.7.0", "decode-formdata": "^0.8.0" }, "devDependencies": { diff --git a/packages/react-form/tests/useForm.test.tsx b/packages/react-form/tests/useForm.test.tsx index 7dfb3c456..f500f5c85 100644 --- a/packages/react-form/tests/useForm.test.tsx +++ b/packages/react-form/tests/useForm.test.tsx @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest' import { render, waitFor } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { useStore } from '@tanstack/react-store' +import { useEffect } from 'react' import { useForm } from '../src/index' import { sleep } from './utils' @@ -732,4 +733,35 @@ describe('useForm', () => { expect(formError.textContent).toBe('Something went wrong'), ) }) + + it('should not cause infinite re-renders when listening to state.errors', () => { + const fn = vi.fn() + + function Comp() { + const form = useForm({ + defaultValues: { + firstName: '', + lastName: '', + }, + onSubmit: async ({ value }) => { + // Do something with form data + console.log(value) + }, + }) + + const { errors } = useStore(form.store, (state) => ({ + errors: state.errors, + })) + + useEffect(() => { + fn(errors) + }, [errors]) + + return null + } + + render() + + expect(fn).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/solid-form/package.json b/packages/solid-form/package.json index d8254a44a..cb32f1ceb 100644 --- a/packages/solid-form/package.json +++ b/packages/solid-form/package.json @@ -57,7 +57,7 @@ ], "dependencies": { "@tanstack/form-core": "workspace:*", - "@tanstack/solid-store": "^0.6.0" + "@tanstack/solid-store": "^0.7.0" }, "devDependencies": { "solid-js": "^1.9.3", diff --git a/packages/solid-form/src/createField.tsx b/packages/solid-form/src/createField.tsx index 25412e13a..338d20c26 100644 --- a/packages/solid-form/src/createField.tsx +++ b/packages/solid-form/src/createField.tsx @@ -112,16 +112,27 @@ export function createField< extendedApi.Field = Field as never + let mounted = false + // Instantiates field meta and removes it when unrendered + onMount(() => { + const cleanupFn = api.mount() + mounted = true + onCleanup(() => { + cleanupFn() + mounted = false + }) + }) + /** * fieldApi.update should not have any side effects. Think of it like a `useRef` * that we need to keep updated every render with the most up-to-date information. * * createComputed to make sure this effect runs before render effects */ - createComputed(() => api.update(opts())) - - // Instantiates field meta and removes it when unrendered - onMount(() => onCleanup(api.mount())) + createComputed(() => { + if (!mounted) return + api.update(opts()) + }) return makeFieldReactive(extendedApi as never) } diff --git a/packages/vue-form/package.json b/packages/vue-form/package.json index f70349ad2..9866b942b 100644 --- a/packages/vue-form/package.json +++ b/packages/vue-form/package.json @@ -54,7 +54,7 @@ ], "dependencies": { "@tanstack/form-core": "workspace:*", - "@tanstack/vue-store": "^0.6.0" + "@tanstack/vue-store": "^0.7.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef459b610..9480198af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -499,8 +499,8 @@ importers: specifier: ^0.40.4 version: link:../../../packages/react-form '@tanstack/react-store': - specifier: ^0.6.1 - version: 0.6.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^0.7.0 + version: 0.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: 15.0.3 version: 15.0.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7) @@ -567,8 +567,8 @@ importers: specifier: ^0.40.4 version: link:../../../packages/react-form '@tanstack/react-store': - specifier: ^0.6.1 - version: 0.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^0.7.0 + version: 0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) isbot: specifier: ^5.1.17 version: 5.1.17 @@ -669,8 +669,8 @@ importers: specifier: ^1.81.1 version: 1.81.1(@tanstack/router-generator@1.79.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-store': - specifier: ^0.6.1 - version: 0.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^0.7.0 + version: 0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/start': specifier: ^1.81.1 version: 1.81.1(@types/node@22.10.1)(encoding@0.1.13)(ioredis@5.4.1)(less@4.2.0)(magicast@0.3.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0))(webpack-sources@3.2.3)(webpack@5.96.1(@swc/core@1.7.42(@swc/helpers@0.5.13))(esbuild@0.24.0)) @@ -1101,8 +1101,8 @@ importers: packages/angular-form: dependencies: '@tanstack/angular-store': - specifier: ^0.6.0 - version: 0.6.0(@angular/common@19.0.1(@angular/core@19.0.1(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.0.1(rxjs@7.8.1)(zone.js@0.15.0)) + specifier: ^0.7.0 + version: 0.7.0(@angular/common@19.0.1(@angular/core@19.0.1(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.0.1(rxjs@7.8.1)(zone.js@0.15.0)) '@tanstack/form-core': specifier: workspace:* version: link:../form-core @@ -1147,8 +1147,8 @@ importers: packages/form-core: dependencies: '@tanstack/store': - specifier: ^0.6.0 - version: 0.6.0 + specifier: ^0.7.0 + version: 0.7.0 devDependencies: arktype: specifier: 2.0.0-rc.23 @@ -1179,8 +1179,8 @@ importers: specifier: workspace:* version: link:../form-core '@tanstack/react-store': - specifier: ^0.6.1 - version: 0.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^0.7.0 + version: 0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) decode-formdata: specifier: ^0.8.0 version: 0.8.0 @@ -1216,8 +1216,8 @@ importers: specifier: workspace:* version: link:../form-core '@tanstack/solid-store': - specifier: ^0.6.0 - version: 0.6.0(solid-js@1.9.3) + specifier: ^0.7.0 + version: 0.7.0(solid-js@1.9.3) devDependencies: solid-js: specifier: ^1.9.3 @@ -1248,8 +1248,8 @@ importers: specifier: workspace:* version: link:../form-core '@tanstack/vue-store': - specifier: ^0.6.0 - version: 0.6.0(vue@3.5.12(typescript@5.7.2)) + specifier: ^0.7.0 + version: 0.7.0(vue@3.5.12(typescript@5.7.2)) devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.1 @@ -4336,8 +4336,8 @@ packages: '@swc/types@0.1.13': resolution: {integrity: sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q==} - '@tanstack/angular-store@0.6.0': - resolution: {integrity: sha512-8fh+SEoWe69reFtb7cS0jpJ07e5n4Wk8cyUgrf8yiUHwUnTBMtbmNgTYsUCL/iCWDUfinm88eUpN5TLP6iXRUA==} + '@tanstack/angular-store@0.7.0': + resolution: {integrity: sha512-Ybl3fCZpfubPDQPbhhvpLGHFx2FRwQHv5bi5tluOtlkTZw3gVxuF+rMxVHfvm3CTI418W7VwiRfPz8//8Gxvkw==} peerDependencies: '@angular/common': '>=19.0.0' '@angular/core': '>=19.0.0' @@ -4382,8 +4382,8 @@ packages: react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 - '@tanstack/react-store@0.6.1': - resolution: {integrity: sha512-6gOopOpPp1cAXkEyTEv6tMbAywwFunvIdCKN/SpEiButUayjXU+Q5Sp5Y3hREN3VMR4OA5+RI5SPhhJoqP9e4w==} + '@tanstack/react-store@0.7.0': + resolution: {integrity: sha512-S/Rq17HaGOk+tQHV/yrePMnG1xbsKZIl/VsNWnNXt4XW+tTY8dTlvpJH2ZQ3GRALsusG5K6Q3unAGJ2pd9W/Ng==} 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 @@ -4407,8 +4407,8 @@ packages: webpack: optional: true - '@tanstack/solid-store@0.6.0': - resolution: {integrity: sha512-2lkalYD/au4PiMWm7Q26FiwLW3DO1xRACY7cPwKMud8rPFlB4tV7SNPq1j41/wtRayFCkR2MOe1+msW1TmMvYw==} + '@tanstack/solid-store@0.7.0': + resolution: {integrity: sha512-uDQYkUuH3MppitiduZLTEcItkTr8vEJ33jzp2rH2VvlNRMGbuU54GQcqf3dLIlTbZ1/Z2TtIBtBjjl+N/OhwRg==} peerDependencies: solid-js: ^1.6.0 @@ -4426,15 +4426,15 @@ packages: '@tanstack/store@0.5.5': resolution: {integrity: sha512-EOSrgdDAJExbvRZEQ/Xhh9iZchXpMN+ga1Bnk8Nmygzs8TfiE6hbzThF+Pr2G19uHL6+DTDTHhJ8VQiOd7l4tA==} - '@tanstack/store@0.6.0': - resolution: {integrity: sha512-+m2OBglsjXcLmmKOX6/9v8BDOCtyxhMmZLsRUDswOOSdIIR9mvv6i0XNKsmTh3AlYU8c1mRcodC8/Vyf+69VlQ==} + '@tanstack/store@0.7.0': + resolution: {integrity: sha512-CNIhdoUsmD2NolYuaIs8VfWM467RK6oIBAW4nPEKZhg1smZ+/CwtCdpURgp7nxSqOaV9oKkzdWD80+bC66F/Jg==} '@tanstack/virtual-file-routes@1.64.0': resolution: {integrity: sha512-soW+gE9QTmMaqXM17r7y1p8NiQVIIECjdTaYla8BKL5Flj030m3KuxEQoiG1XgjtA0O7ayznFz2YvPcXIy3qDg==} engines: {node: '>=12'} - '@tanstack/vue-store@0.6.0': - resolution: {integrity: sha512-QcvBEOG2dPiFw06LqEk1ehL7ZY0CJS8AKlW7uqmFFFzVost+Oj1oiQne9hW+L6O8f0ZnwFlD9fA5a7pTum8YeQ==} + '@tanstack/vue-store@0.7.0': + resolution: {integrity: sha512-oLB/WuD26caR86rxLz39LvS5YdY0KIThJFEHIW/mXujC2+M/z3GxVZFJsZianAzr3tH56sZQ8kkq4NvwwsOBkQ==} peerDependencies: '@vue/composition-api': ^1.2.1 vue: ^2.5.0 || ^3.0.0 @@ -10060,6 +10060,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -13592,11 +13597,11 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/angular-store@0.6.0(@angular/common@19.0.1(@angular/core@19.0.1(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.0.1(rxjs@7.8.1)(zone.js@0.15.0))': + '@tanstack/angular-store@0.7.0(@angular/common@19.0.1(@angular/core@19.0.1(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1))(@angular/core@19.0.1(rxjs@7.8.1)(zone.js@0.15.0))': dependencies: '@angular/common': 19.0.1(@angular/core@19.0.1(rxjs@7.8.1)(zone.js@0.15.0))(rxjs@7.8.1) '@angular/core': 19.0.1(rxjs@7.8.1)(zone.js@0.15.0) - '@tanstack/store': 0.6.0 + '@tanstack/store': 0.7.0 tslib: 2.8.1 '@tanstack/config@0.14.2(@types/node@22.10.1)(esbuild@0.24.0)(eslint@9.16.0(jiti@2.4.0))(rollup@4.26.0)(typescript@5.6.3)(vite@5.4.11(@types/node@22.10.1)(less@4.2.0)(sass@1.80.7)(sugarss@4.0.1(postcss@8.4.49))(terser@5.36.0))': @@ -13666,19 +13671,19 @@ snapshots: react-dom: 18.3.1(react@18.3.1) use-sync-external-store: 1.2.2(react@18.3.1) - '@tanstack/react-store@0.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-store@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/store': 0.6.0 + '@tanstack/store': 0.7.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - use-sync-external-store: 1.2.2(react@18.3.1) + use-sync-external-store: 1.4.0(react@18.3.1) - '@tanstack/react-store@0.6.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + '@tanstack/react-store@0.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: - '@tanstack/store': 0.6.0 + '@tanstack/store': 0.7.0 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - use-sync-external-store: 1.2.2(react@19.0.0) + use-sync-external-store: 1.4.0(react@19.0.0) '@tanstack/router-generator@1.79.0': dependencies: @@ -13714,9 +13719,9 @@ snapshots: - supports-color - webpack-sources - '@tanstack/solid-store@0.6.0(solid-js@1.9.3)': + '@tanstack/solid-store@0.7.0(solid-js@1.9.3)': dependencies: - '@tanstack/store': 0.6.0 + '@tanstack/store': 0.7.0 solid-js: 1.9.3 '@tanstack/start-vite-plugin@1.79.0': @@ -13794,13 +13799,13 @@ snapshots: '@tanstack/store@0.5.5': {} - '@tanstack/store@0.6.0': {} + '@tanstack/store@0.7.0': {} '@tanstack/virtual-file-routes@1.64.0': {} - '@tanstack/vue-store@0.6.0(vue@3.5.12(typescript@5.7.2))': + '@tanstack/vue-store@0.7.0(vue@3.5.12(typescript@5.7.2))': dependencies: - '@tanstack/store': 0.6.0 + '@tanstack/store': 0.7.0 vue: 3.5.12(typescript@5.7.2) vue-demi: 0.14.10(vue@3.5.12(typescript@5.7.2)) @@ -20471,7 +20476,11 @@ snapshots: dependencies: react: 18.3.1 - use-sync-external-store@1.2.2(react@19.0.0): + use-sync-external-store@1.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + + use-sync-external-store@1.4.0(react@19.0.0): dependencies: react: 19.0.0