From f43e9a07d513398d0a22161df964358a6fa97c8f Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 30 Nov 2025 00:27:20 -0800 Subject: [PATCH 01/18] test: add failing test for merge-form behavior --- packages/form-core/tests/FormApi.spec.ts | 40 ++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index dd418d727..9d7e87cea 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest' import { z } from 'zod' -import { FieldApi, FormApi, formEventClient } from '../src/index' +import { FieldApi, FormApi, formEventClient, mergeForm } from '../src/index' import { sleep } from './utils' -import type { AnyFieldApi, AnyFormApi } from '../src/index' +import type { AnyFieldApi, AnyFormApi, AnyFormState, FormState } from '../src/index' describe('form api', () => { it('should get default form state when default values are passed', () => { @@ -4113,3 +4113,39 @@ describe('form api event client', () => { logSpy.mockRestore() }) }) + +it("transform option does not invalidate state for the field", () => { + const state = {current: {}} as {current: Partial}; + const form = new FormApi({ + defaultValues: { + age: 0, + }, + transform: { + fn: f => mergeForm(f as never, state.current) as never, + deps: [state.current] + } + }) + + form.mount() + + const ageField = new FieldApi({ + form, + name: 'age', + }); + + ageField.mount() + + expect(ageField.state.meta.isValid).toBe(true); + + state.current = { + errorMap: { + onServer: { + fields: { + age: 'Age is invalid from server', + } + } + } + }; + + expect(ageField.state.meta.isValid).toBe(false); +}) \ No newline at end of file From 44e6f2bb5bc0378b7779bf2d1036b74ab6bfe05d Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 30 Nov 2025 01:09:16 -0800 Subject: [PATCH 02/18] chore: attempt 1 --- packages/form-core/src/FormApi.ts | 88 +++++++++++++++++++++++- packages/form-core/tests/FormApi.spec.ts | 40 +++++++---- 2 files changed, 112 insertions(+), 16 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index b4887b97d..47c82eb33 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -528,6 +528,37 @@ export type AnyFormOptions = FormOptions< any > +function trackDeps(depz: unknown[], fn: () => void) { + // Track referential changes to items in the array + return new Proxy(depz, { + set(target, prop, value) { + const oldValue = target[prop as keyof typeof target] + const result = Reflect.set(target, prop, value) + // Check if the reference changed (not just the value) + if (oldValue !== value) { + fn() + } + return result + }, + // Add a special `__IS_TANSTACK_FORM_PROXY__` property to identify the proxy + get(target, prop, receiver) { + if (prop === '__IS_TANSTACK_FORM_PROXY__') { + return true + } + return Reflect.get(target, prop, receiver) + }, + }) +} + +function isTrackedDeps(depz: unknown[]) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + return !!( + depz && + typeof depz === 'object' && + '__IS_TANSTACK_FORM_PROXY__' in depz + ) +} + /** * An object representing the validation metadata for a field. Not intended for public usage. */ @@ -895,7 +926,7 @@ export class FormApi< /** * The options for the form. */ - options: FormOptions< + private _options: FormOptions< TFormData, TOnMount, TOnChange, @@ -909,6 +940,41 @@ export class FormApi< TOnServer, TSubmitMeta > = {} + + get options() { + return Object.assign( + {}, + this._options, + this._options.transform?.deps + ? { + transform: { + deps: this._prevTransformDeps, + fn: this._options.transform.fn, + }, + } + : {}, + ) + } + + set options( + val: FormOptions< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + >, + ) { + this._options = val + } + baseStore!: Store< BaseFormState< TFormData, @@ -985,6 +1051,8 @@ export class FormApi< */ private _devtoolsSubmissionOverride: boolean + private _prevTransformDeps: unknown[] | null = null + /** * Constructs a new `FormApi` instance with the given form options. */ @@ -1014,6 +1082,15 @@ export class FormApi< this._devtoolsSubmissionOverride = false + if (opts?.transform?.deps) { + this._prevTransformDeps = trackDeps(opts.transform.deps, () => { + this.baseStore.setState((prevState) => ({ + ...prevState, + _force_re_eval: !(prevState._force_re_eval ?? false), + })) + }) + } + this.baseStore = new Store( getDefaultFormState({ ...(opts?.defaultState as any), @@ -1447,6 +1524,15 @@ export class FormApi< !evaluate(options.defaultState, oldOptions.defaultState) && !this.state.isTouched + if (options.transform?.deps && !isTrackedDeps(options.transform.deps)) { + this._prevTransformDeps = trackDeps(options.transform.deps, () => { + this.baseStore.setState((prevState) => ({ + ...prevState, + _force_re_eval: !(prevState._force_re_eval ?? false), + })) + }) + } + if (!shouldUpdateValues && !shouldUpdateState && !shouldUpdateReeval) return batch(() => { diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 9d7e87cea..4cd8fadac 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -2,7 +2,12 @@ import { describe, expect, it, vi } from 'vitest' import { z } from 'zod' import { FieldApi, FormApi, formEventClient, mergeForm } from '../src/index' import { sleep } from './utils' -import type { AnyFieldApi, AnyFormApi, AnyFormState, FormState } from '../src/index' +import type { + AnyFieldApi, + AnyFormApi, + AnyFormState, + FormState, +} from '../src/index' describe('form api', () => { it('should get default form state when default values are passed', () => { @@ -4114,16 +4119,17 @@ describe('form api event client', () => { }) }) -it("transform option does not invalidate state for the field", () => { - const state = {current: {}} as {current: Partial}; +it('transform option does not invalidate state for the field', () => { + const state = { current: {} } as { current: Partial } + const form = new FormApi({ defaultValues: { age: 0, }, transform: { - fn: f => mergeForm(f as never, state.current) as never, - deps: [state.current] - } + fn: (f) => mergeForm(f as never, state.current) as never, + deps: [state.current], + }, }) form.mount() @@ -4131,21 +4137,25 @@ it("transform option does not invalidate state for the field", () => { const ageField = new FieldApi({ form, name: 'age', - }); + }) ageField.mount() - expect(ageField.state.meta.isValid).toBe(true); + expect(ageField.state.meta.isValid).toBe(true) state.current = { errorMap: { onServer: { fields: { age: 'Age is invalid from server', - } - } - } - }; - - expect(ageField.state.meta.isValid).toBe(false); -}) \ No newline at end of file + }, + }, + }, + } + + console.log(form.options.transform) + + form.options.transform!.deps[0] = state.current + + expect(ageField.state.meta.isValid).toBe(false) +}) From fa25c4ccac50667adf7873198793ecd002452f3a Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 30 Nov 2025 01:11:06 -0800 Subject: [PATCH 03/18] chore: attempt 2 --- packages/form-core/src/FormApi.ts | 42 +++---------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 47c82eb33..c075bdbb5 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -926,7 +926,7 @@ export class FormApi< /** * The options for the form. */ - private _options: FormOptions< + options: FormOptions< TFormData, TOnMount, TOnChange, @@ -941,40 +941,6 @@ export class FormApi< TSubmitMeta > = {} - get options() { - return Object.assign( - {}, - this._options, - this._options.transform?.deps - ? { - transform: { - deps: this._prevTransformDeps, - fn: this._options.transform.fn, - }, - } - : {}, - ) - } - - set options( - val: FormOptions< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - >, - ) { - this._options = val - } - baseStore!: Store< BaseFormState< TFormData, @@ -1051,8 +1017,6 @@ export class FormApi< */ private _devtoolsSubmissionOverride: boolean - private _prevTransformDeps: unknown[] | null = null - /** * Constructs a new `FormApi` instance with the given form options. */ @@ -1083,7 +1047,7 @@ export class FormApi< this._devtoolsSubmissionOverride = false if (opts?.transform?.deps) { - this._prevTransformDeps = trackDeps(opts.transform.deps, () => { + opts.transform.deps = trackDeps(opts.transform.deps, () => { this.baseStore.setState((prevState) => ({ ...prevState, _force_re_eval: !(prevState._force_re_eval ?? false), @@ -1525,7 +1489,7 @@ export class FormApi< !this.state.isTouched if (options.transform?.deps && !isTrackedDeps(options.transform.deps)) { - this._prevTransformDeps = trackDeps(options.transform.deps, () => { + options.transform.deps = trackDeps(options.transform.deps, () => { this.baseStore.setState((prevState) => ({ ...prevState, _force_re_eval: !(prevState._force_re_eval ?? false), From 6e713a2004f7f6e916973722478abcb94976139d Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 30 Nov 2025 01:17:02 -0800 Subject: [PATCH 04/18] chore: attempt 3 --- packages/form-core/src/FormApi.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index c075bdbb5..5c39d87dd 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1273,6 +1273,12 @@ export class FormApi< 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]) + if ( prevVal && prevBaseStore && @@ -1289,6 +1295,7 @@ export class FormApi< prevVal.isPristine === isPristine && prevVal.isDefaultValue === isDefaultValue && prevVal.isDirty === isDirty && + !shouldTransform && evaluate(prevBaseStore, currBaseStore) ) { return prevVal @@ -1323,18 +1330,12 @@ export class FormApi< TOnServer > - // Only run transform if state has shallowly changed - IE how React.useEffect works - const transformArray = this.options.transform?.deps ?? [] - const shouldTransform = - transformArray.length !== this.prevTransformArray.length || - transformArray.some((val, i) => val !== this.prevTransformArray[i]) - if (shouldTransform) { const newObj = Object.assign({}, this, { state }) // This mutates the state this.options.transform?.fn(newObj) state = newObj.state - this.prevTransformArray = transformArray + this.prevTransformArray = [...transformArray] } return state From 67e0820ab3b5754cfe08bc377249e91ebb1eed93 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 30 Nov 2025 01:22:06 -0800 Subject: [PATCH 05/18] chore: attempt 4 --- packages/form-core/src/FormApi.ts | 62 ++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 5c39d87dd..117b3c1a8 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -956,6 +956,21 @@ export class FormApi< TOnServer > > + mergedBaseStore!: Derived< + BaseFormState< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer + > + > fieldMetaDerived: Derived< FormState< TFormData, @@ -1063,8 +1078,32 @@ export class FormApi< }), ) - this.fieldMetaDerived = new Derived({ + this.mergedBaseStore = new Derived({ deps: [this.baseStore], + fn: ({ currDepVals }) => { + const currBaseStore = currDepVals[0] + + // 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: currBaseStore }) + // This mutates the state + this.options.transform?.fn(newObj) + const state = newObj.state + this.prevTransformArray = [...transformArray] + return state + } + + return currBaseStore + }, + }) + + this.fieldMetaDerived = new Derived({ + deps: [this.mergedBaseStore], fn: ({ prevDepVals, currDepVals, prevVal: _prevVal }) => { const prevVal = _prevVal as | Record, AnyFieldMeta> @@ -1172,7 +1211,7 @@ export class FormApi< }) this.store = new Derived({ - deps: [this.baseStore, this.fieldMetaDerived], + deps: [this.mergedBaseStore, this.fieldMetaDerived], fn: ({ prevDepVals, currDepVals, prevVal: _prevVal }) => { const prevVal = _prevVal as | FormState< @@ -1273,12 +1312,6 @@ export class FormApi< 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]) - if ( prevVal && prevBaseStore && @@ -1295,13 +1328,12 @@ export class FormApi< prevVal.isPristine === isPristine && prevVal.isDefaultValue === isDefaultValue && prevVal.isDirty === isDirty && - !shouldTransform && evaluate(prevBaseStore, currBaseStore) ) { return prevVal } - let state = { + const state = { ...currBaseStore, errorMap, fieldMeta: this.fieldMetaDerived.state, @@ -1330,14 +1362,6 @@ export class FormApi< TOnServer > - if (shouldTransform) { - const newObj = Object.assign({}, this, { state }) - // This mutates the state - this.options.transform?.fn(newObj) - state = newObj.state - this.prevTransformArray = [...transformArray] - } - return state }, }) @@ -1418,9 +1442,11 @@ export class FormApi< } mount = () => { + const cleanupMergedBaseStoreDerived = this.mergedBaseStore.mount() const cleanupFieldMetaDerived = this.fieldMetaDerived.mount() const cleanupStoreDerived = this.store.mount() const cleanup = () => { + cleanupMergedBaseStoreDerived() cleanupFieldMetaDerived() cleanupStoreDerived() From 7782f5605850d4f9e3c67c8b8725eafb5a8c2893 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 30 Nov 2025 01:26:28 -0800 Subject: [PATCH 06/18] chore: attempt 5 --- packages/form-core/src/FormApi.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 117b3c1a8..32cc20e50 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1080,14 +1080,17 @@ export class FormApi< this.mergedBaseStore = new Derived({ deps: [this.baseStore], - fn: ({ currDepVals }) => { + fn: ({ currDepVals, prevDepVals }) => { const currBaseStore = currDepVals[0] + const prevBaseStore = prevDepVals?.[0] // Only run transform if state has shallowly changed - IE how React.useEffect works - const transformArray = this.options.transform?.deps ?? [] + const transformArray = this.options.transform?.deps const shouldTransform = - transformArray.length !== this.prevTransformArray.length || - transformArray.some((val, i) => val !== this.prevTransformArray[i]) + transformArray && + currBaseStore !== prevBaseStore && + (transformArray.length !== this.prevTransformArray.length || + transformArray.some((val, i) => val !== this.prevTransformArray[i])) if (shouldTransform) { const newObj = Object.assign({}, this, { state: currBaseStore }) From bf888d6a1ec60d89d036a66070e2fc9328049786 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 30 Nov 2025 01:36:20 -0800 Subject: [PATCH 07/18] CHORE!: REMOVE THIS ATTEMP --- packages/form-core/src/FormApi.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 32cc20e50..3bf0f4dbc 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1093,7 +1093,11 @@ export class FormApi< transformArray.some((val, i) => val !== this.prevTransformArray[i])) if (shouldTransform) { - const newObj = Object.assign({}, this, { state: currBaseStore }) + const newObj = Object.assign({}, this, { + // structuredClone is required to avoid `state` being mutated outside of this block + // Commonly available since 2022 in all major browsers BUT NOT REACT NATIVE NOOOOOOO + state: structuredClone(currBaseStore), + }) // This mutates the state this.options.transform?.fn(newObj) const state = newObj.state From 20db3db683f8e78404364435c1e6ff11d4c38528 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 30 Nov 2025 01:52:34 -0800 Subject: [PATCH 08/18] chore: attempt 6 --- packages/form-core/src/FormApi.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 3bf0f4dbc..5a6873ace 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1087,10 +1087,12 @@ export class FormApi< // Only run transform if state has shallowly changed - IE how React.useEffect works const transformArray = this.options.transform?.deps const shouldTransform = - transformArray && - currBaseStore !== prevBaseStore && - (transformArray.length !== this.prevTransformArray.length || - transformArray.some((val, i) => val !== this.prevTransformArray[i])) + (transformArray && currBaseStore !== prevBaseStore) || + (transformArray && + (transformArray.length !== this.prevTransformArray.length || + transformArray.some( + (val, i) => val !== this.prevTransformArray[i], + ))) if (shouldTransform) { const newObj = Object.assign({}, this, { From 464a129ac011447364dedb69c57d09fd4dd7bd70 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 30 Nov 2025 02:04:50 -0800 Subject: [PATCH 09/18] WIP: TO REVERT: Temp replace Next action with failing usage --- .../react/next-server-actions/package.json | 5 +- .../next-server-actions/src/app/action.ts | 28 +- .../src/app/client-component.tsx | 45 +-- .../react/next-server-actions/tsconfig.json | 20 +- pnpm-lock.yaml | 381 +++++++++--------- 5 files changed, 253 insertions(+), 226 deletions(-) diff --git a/examples/react/next-server-actions/package.json b/examples/react/next-server-actions/package.json index 86adbe823..3e41f0c67 100644 --- a/examples/react/next-server-actions/package.json +++ b/examples/react/next-server-actions/package.json @@ -10,9 +10,10 @@ "dependencies": { "@tanstack/react-form-nextjs": "^1.26.0", "@tanstack/react-store": "^0.7.7", - "next": "15.5.3", + "next": "16.0.5", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "zod": "^3.25.76" }, "devDependencies": { "@types/node": "^24.1.0", diff --git a/examples/react/next-server-actions/src/app/action.ts b/examples/react/next-server-actions/src/app/action.ts index 69ea1fd40..2ccb9e7ab 100644 --- a/examples/react/next-server-actions/src/app/action.ts +++ b/examples/react/next-server-actions/src/app/action.ts @@ -1,24 +1,26 @@ -'use server' +'use server'; import { ServerValidateError, createServerValidate, -} from '@tanstack/react-form-nextjs' -import { formOpts } from './shared-code' +} from '@tanstack/react-form-nextjs'; +import { formOpts } from './shared-code'; +import { z } from 'zod'; + +const schema = z.object({ + age: z.number().min(12), + firstName: z.string(), +}); const serverValidate = createServerValidate({ ...formOpts, - onServerValidate: ({ value }) => { - if (value.age < 12) { - return 'Server validation: You must be at least 12 to sign up' - } - }, -}) + onServerValidate: schema, +}); export default async function someAction(prev: unknown, formData: FormData) { try { - const validatedData = await serverValidate(formData) - console.log('validatedData', validatedData) + const validatedData = await serverValidate(formData); + console.log('validatedData', validatedData); // Persist the form data to the database // await sql` // INSERT INTO users (name, email, password) @@ -26,11 +28,11 @@ export default async function someAction(prev: unknown, formData: FormData) { // ` } catch (e) { if (e instanceof ServerValidateError) { - return e.formState + return e.formState; } // Some other error occurred while validating your form - throw e + throw e; } // Your form has successfully validated! diff --git a/examples/react/next-server-actions/src/app/client-component.tsx b/examples/react/next-server-actions/src/app/client-component.tsx index 2b071ef53..c1b687bd7 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -1,56 +1,47 @@ -'use client' +'use client'; -import { useActionState } from 'react' -import { - initialFormState, - mergeForm, - useForm, - useTransform, -} from '@tanstack/react-form-nextjs' -import { useStore } from '@tanstack/react-store' -import someAction from './action' -import { formOpts } from './shared-code' +import { useActionState } from 'react'; +import { mergeForm, useForm } from '@tanstack/react-form-nextjs'; +import { initialFormState, useTransform } from '@tanstack/react-form-nextjs'; +import someAction from './action'; +import { formOpts } from './shared-code'; +import { z } from 'zod'; export const ClientComp = () => { - const [state, action] = useActionState(someAction, initialFormState) + const [state, action] = useActionState(someAction, initialFormState); + + debugger const form = useForm({ ...formOpts, transform: useTransform( (baseForm) => mergeForm(baseForm, state ?? {}), - [state], + [state] ), - }) - - const formErrors = useStore(form.store, (formState) => formState.errors) + }); return (
form.handleSubmit()}> - {formErrors.map((error) => ( -

{error}

- ))} - - value < 8 ? 'Client validation: You must be at least 8' : undefined, + onChange: z.coerce.number().min(8), }} > {(field) => { return (
field.handleChange(e.target.valueAsNumber)} /> {field.state.meta.errors.map((error) => ( -

{error}

+

{error?.message}

))}
- ) + ); }}
{ )}
- ) -} + ); +}; diff --git a/examples/react/next-server-actions/tsconfig.json b/examples/react/next-server-actions/tsconfig.json index 28a97cedc..5ba546ad3 100644 --- a/examples/react/next-server-actions/tsconfig.json +++ b/examples/react/next-server-actions/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "es5", - "lib": ["DOM", "DOM.Iterable", "ESNext"], + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -11,7 +15,7 @@ "moduleResolution": "Bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { @@ -19,6 +23,14 @@ } ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1629f5386..297d01c03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -584,14 +584,17 @@ importers: specifier: ^0.7.7 version: 0.7.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) next: - specifier: 15.5.3 - version: 15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0) + specifier: 16.0.5 + version: 16.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0) react: specifier: ^19.0.0 version: 19.1.0 react-dom: specifier: ^19.0.0 version: 19.1.0(react@19.1.0) + zod: + specifier: ^3.25.76 + version: 3.25.76 devDependencies: '@types/node': specifier: ^24.1.0 @@ -2495,6 +2498,9 @@ packages: '@emnapi/runtime@1.5.0': resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -3094,124 +3100,139 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@img/sharp-darwin-arm64@0.34.3': - resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.3': - resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.0': - resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.0': - resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.0': - resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.2.0': - resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.2.0': - resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.2.0': - resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.2.0': - resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': - resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.0': - resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.3': - resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.3': - resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-ppc64@0.34.3': - resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - '@img/sharp-linux-s390x@0.34.3': - resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.3': - resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.3': - resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.3': - resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.3': - resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.3': - resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.3': - resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.3': - resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -3745,53 +3766,53 @@ packages: '@napi-rs/wasm-runtime@1.0.7': resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} - '@next/env@15.5.3': - resolution: {integrity: sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==} + '@next/env@16.0.5': + resolution: {integrity: sha512-jRLOw822AE6aaIm9oh0NrauZEM0Vtx5xhYPgqx89txUmv/UmcRwpcXmGeQOvYNT/1bakUwA+nG5CA74upYVVDw==} - '@next/swc-darwin-arm64@15.5.3': - resolution: {integrity: sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==} + '@next/swc-darwin-arm64@16.0.5': + resolution: {integrity: sha512-65Mfo1rD+mVbJuBTlXbNelNOJ5ef+5pskifpFHsUt3cnOWjDNKctHBwwSz9tJlPp7qADZtiN/sdcG7mnc0El8Q==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.5.3': - resolution: {integrity: sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==} + '@next/swc-darwin-x64@16.0.5': + resolution: {integrity: sha512-2fDzXD/JpEjY500VUF0uuGq3YZcpC6XxmGabePPLyHCKbw/YXRugv3MRHH7MxE2hVHtryXeSYYnxcESb/3OUIQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.3': - resolution: {integrity: sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==} + '@next/swc-linux-arm64-gnu@16.0.5': + resolution: {integrity: sha512-meSLB52fw4tgDpPnyuhwA280EWLwwIntrxLYjzKU3e3730ur2WJAmmqoZ1LPIZ2l3eDfh9SBHnJGTczbgPeNeA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.5.3': - resolution: {integrity: sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==} + '@next/swc-linux-arm64-musl@16.0.5': + resolution: {integrity: sha512-aAJtQkvUzz5t0xVAmK931SIhWnSQAaEoTyG/sKPCYq2u835K/E4a14A+WRPd4dkhxIHNudE8dI+FpHekgdrA4g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.5.3': - resolution: {integrity: sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==} + '@next/swc-linux-x64-gnu@16.0.5': + resolution: {integrity: sha512-bYwbjBwooMWRhy6vRxenaYdguTM2hlxFt1QBnUF235zTnU2DhGpETm5WU93UvtAy0uhC5Kgqsl8RyNXlprFJ6Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.5.3': - resolution: {integrity: sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==} + '@next/swc-linux-x64-musl@16.0.5': + resolution: {integrity: sha512-iGv2K/4gW3mkzh+VcZTf2gEGX5o9xdb5oPqHjgZvHdVzCw0iSAJ7n9vKzl3SIEIIHZmqRsgNasgoLd0cxaD+tg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.5.3': - resolution: {integrity: sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==} + '@next/swc-win32-arm64-msvc@16.0.5': + resolution: {integrity: sha512-6xf52Hp4SH9+4jbYmfUleqkuxvdB9JJRwwFlVG38UDuEGPqpIA+0KiJEU9lxvb0RGNo2i2ZUhc5LHajij9H9+A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.5.3': - resolution: {integrity: sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==} + '@next/swc-win32-x64-msvc@16.0.5': + resolution: {integrity: sha512-06kTaOh+Qy/kguN+MMK+/VtKmRkQJrPlGQMvCUbABk1UxI5SKTgJhbmMj9Hf0qWwrS6g9JM6/Zk+etqeMyvHAw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -6029,13 +6050,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -6347,6 +6361,10 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -7352,9 +7370,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-bigint@1.1.0: resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} engines: {node: '>= 0.4'} @@ -8312,9 +8327,9 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@15.5.3: - resolution: {integrity: sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + next@16.0.5: + resolution: {integrity: sha512-XUPsFqSqu/NDdPfn/cju9yfIedkDI7ytDoALD9todaSMxk1Z5e3WcbUjfI9xsanFTys7xz62lnRWNFqJordzkQ==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -9327,6 +9342,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} @@ -9381,8 +9401,8 @@ packages: resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} engines: {node: '>=8'} - sharp@0.34.3: - resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: @@ -9471,9 +9491,6 @@ packages: resolution: {integrity: sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==} engines: {node: ^18.17.0 || >=20.5.0} - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -12123,6 +12140,11 @@ snapshots: dependencies: tslib: 2.8.1 + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.1.0': dependencies: tslib: 2.8.1 @@ -12605,90 +12627,101 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@img/sharp-darwin-arm64@0.34.3': + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.34.3': + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-arm64@1.2.0': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.2.0': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.2.0': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.2.0': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.0': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.2.0': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.2.0': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.0': + '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true - '@img/sharp-linux-arm64@0.34.3': + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.34.3': + '@img/sharp-linux-arm@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-ppc64@0.34.3': + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.34.3': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.34.3': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.3': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.3': + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-wasm32@0.34.3': + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.7.1 optional: true - '@img/sharp-win32-arm64@0.34.3': + '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-ia32@0.34.3': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-x64@0.34.3': + '@img/sharp-win32-x64@0.34.5': optional: true '@inquirer/checkbox@4.2.2(@types/node@24.1.0)': @@ -13230,30 +13263,30 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@15.5.3': {} + '@next/env@16.0.5': {} - '@next/swc-darwin-arm64@15.5.3': + '@next/swc-darwin-arm64@16.0.5': optional: true - '@next/swc-darwin-x64@15.5.3': + '@next/swc-darwin-x64@16.0.5': optional: true - '@next/swc-linux-arm64-gnu@15.5.3': + '@next/swc-linux-arm64-gnu@16.0.5': optional: true - '@next/swc-linux-arm64-musl@15.5.3': + '@next/swc-linux-arm64-musl@16.0.5': optional: true - '@next/swc-linux-x64-gnu@15.5.3': + '@next/swc-linux-x64-gnu@16.0.5': optional: true - '@next/swc-linux-x64-musl@15.5.3': + '@next/swc-linux-x64-musl@16.0.5': optional: true - '@next/swc-win32-arm64-msvc@15.5.3': + '@next/swc-win32-arm64-msvc@16.0.5': optional: true - '@next/swc-win32-x64-msvc@15.5.3': + '@next/swc-win32-x64-msvc@16.0.5': optional: true '@ngtools/webpack@20.3.6(@angular/compiler-cli@20.3.6(@angular/compiler@20.3.6)(typescript@5.8.2))(typescript@5.8.2)(webpack@5.101.2(@swc/core@1.13.5)(esbuild@0.25.9))': @@ -15874,18 +15907,6 @@ snapshots: color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - optional: true - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - optional: true - colorette@2.0.20: {} combined-stream@1.0.8: @@ -16171,6 +16192,9 @@ snapshots: detect-libc@2.0.4: optional: true + detect-libc@2.1.2: + optional: true + detect-node-es@1.1.0: {} detect-node@2.1.0: {} @@ -17406,9 +17430,6 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: - optional: true - is-bigint@1.1.0: dependencies: has-bigints: 1.1.0 @@ -18541,9 +18562,9 @@ snapshots: neo-async@2.6.2: {} - next@15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0): + next@16.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.90.0): dependencies: - '@next/env': 15.5.3 + '@next/env': 16.0.5 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001739 postcss: 8.4.31 @@ -18551,16 +18572,16 @@ snapshots: react-dom: 19.1.0(react@19.1.0) styled-jsx: 5.1.6(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.3 - '@next/swc-darwin-x64': 15.5.3 - '@next/swc-linux-arm64-gnu': 15.5.3 - '@next/swc-linux-arm64-musl': 15.5.3 - '@next/swc-linux-x64-gnu': 15.5.3 - '@next/swc-linux-x64-musl': 15.5.3 - '@next/swc-win32-arm64-msvc': 15.5.3 - '@next/swc-win32-x64-msvc': 15.5.3 + '@next/swc-darwin-arm64': 16.0.5 + '@next/swc-darwin-x64': 16.0.5 + '@next/swc-linux-arm64-gnu': 16.0.5 + '@next/swc-linux-arm64-musl': 16.0.5 + '@next/swc-linux-x64-gnu': 16.0.5 + '@next/swc-linux-x64-musl': 16.0.5 + '@next/swc-win32-arm64-msvc': 16.0.5 + '@next/swc-win32-x64-msvc': 16.0.5 sass: 1.90.0 - sharp: 0.34.3 + sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -19742,6 +19763,9 @@ snapshots: semver@7.7.2: {} + semver@7.7.3: + optional: true + send@0.19.0: dependencies: debug: 2.6.9 @@ -19842,34 +19866,36 @@ snapshots: dependencies: kind-of: 6.0.3 - sharp@0.34.3: + sharp@0.34.5: dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.3 - '@img/sharp-darwin-x64': 0.34.3 - '@img/sharp-libvips-darwin-arm64': 1.2.0 - '@img/sharp-libvips-darwin-x64': 1.2.0 - '@img/sharp-libvips-linux-arm': 1.2.0 - '@img/sharp-libvips-linux-arm64': 1.2.0 - '@img/sharp-libvips-linux-ppc64': 1.2.0 - '@img/sharp-libvips-linux-s390x': 1.2.0 - '@img/sharp-libvips-linux-x64': 1.2.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 - '@img/sharp-linux-arm': 0.34.3 - '@img/sharp-linux-arm64': 0.34.3 - '@img/sharp-linux-ppc64': 0.34.3 - '@img/sharp-linux-s390x': 0.34.3 - '@img/sharp-linux-x64': 0.34.3 - '@img/sharp-linuxmusl-arm64': 0.34.3 - '@img/sharp-linuxmusl-x64': 0.34.3 - '@img/sharp-wasm32': 0.34.3 - '@img/sharp-win32-arm64': 0.34.3 - '@img/sharp-win32-ia32': 0.34.3 - '@img/sharp-win32-x64': 0.34.3 + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 optional: true shebang-command@2.0.0: @@ -19960,11 +19986,6 @@ snapshots: transitivePeerDependencies: - supports-color - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 - optional: true - slash@3.0.0: {} slice-ansi@5.0.0: From c293607ea70432c89ea660f24d56304f75aa0802 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Sun, 30 Nov 2025 02:04:58 -0800 Subject: [PATCH 10/18] chore: add logs --- packages/form-core/tests/FormApi.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 4cd8fadac..cf98a8f54 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -4153,9 +4153,13 @@ it('transform option does not invalidate state for the field', () => { }, } - console.log(form.options.transform) + console.log(form.mergedBaseStore.state.errorMap.onServer) form.options.transform!.deps[0] = state.current + console.log(form.mergedBaseStore.state.errorMap.onServer) + + console.log(form.fieldMetaDerived) + expect(ageField.state.meta.isValid).toBe(false) }) From 9a622577220563f31e25d25610064dd62916bad2 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 1 Dec 2025 00:55:50 -0800 Subject: [PATCH 11/18] chore: attempt 7 --- packages/form-core/src/FormApi.ts | 185 +++++++++-------------- packages/form-core/tests/FormApi.spec.ts | 31 ++-- 2 files changed, 84 insertions(+), 132 deletions(-) diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 5a6873ace..8c2603ae6 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -263,7 +263,6 @@ export interface FormTransform< TOnServer, TSubmitMeta > - deps: unknown[] } export interface FormListeners< @@ -528,37 +527,6 @@ export type AnyFormOptions = FormOptions< any > -function trackDeps(depz: unknown[], fn: () => void) { - // Track referential changes to items in the array - return new Proxy(depz, { - set(target, prop, value) { - const oldValue = target[prop as keyof typeof target] - const result = Reflect.set(target, prop, value) - // Check if the reference changed (not just the value) - if (oldValue !== value) { - fn() - } - return result - }, - // Add a special `__IS_TANSTACK_FORM_PROXY__` property to identify the proxy - get(target, prop, receiver) { - if (prop === '__IS_TANSTACK_FORM_PROXY__') { - return true - } - return Reflect.get(target, prop, receiver) - }, - }) -} - -function isTrackedDeps(depz: unknown[]) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return !!( - depz && - typeof depz === 'object' && - '__IS_TANSTACK_FORM_PROXY__' in depz - ) -} - /** * An object representing the validation metadata for a field. Not intended for public usage. */ @@ -689,6 +657,20 @@ export type BaseFormState< _force_re_eval?: boolean } +type AnyBaseFormState = BaseFormState< + any, + any, + any, + any, + any, + any, + any, + any, + any, + any, + any +> + export type DerivedFormState< in out TFormData, in out TOnMount extends undefined | FormValidateOrFn, @@ -956,21 +938,6 @@ export class FormApi< TOnServer > > - mergedBaseStore!: Derived< - BaseFormState< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer - > - > fieldMetaDerived: Derived< FormState< TFormData, @@ -1061,15 +1028,6 @@ export class FormApi< this._devtoolsSubmissionOverride = false - if (opts?.transform?.deps) { - opts.transform.deps = trackDeps(opts.transform.deps, () => { - this.baseStore.setState((prevState) => ({ - ...prevState, - _force_re_eval: !(prevState._force_re_eval ?? false), - })) - }) - } - this.baseStore = new Store( getDefaultFormState({ ...(opts?.defaultState as any), @@ -1078,41 +1036,8 @@ export class FormApi< }), ) - this.mergedBaseStore = new Derived({ - deps: [this.baseStore], - fn: ({ currDepVals, prevDepVals }) => { - const currBaseStore = currDepVals[0] - const prevBaseStore = prevDepVals?.[0] - - // Only run transform if state has shallowly changed - IE how React.useEffect works - const transformArray = this.options.transform?.deps - const shouldTransform = - (transformArray && currBaseStore !== prevBaseStore) || - (transformArray && - (transformArray.length !== this.prevTransformArray.length || - transformArray.some( - (val, i) => val !== this.prevTransformArray[i], - ))) - - if (shouldTransform) { - const newObj = Object.assign({}, this, { - // structuredClone is required to avoid `state` being mutated outside of this block - // Commonly available since 2022 in all major browsers BUT NOT REACT NATIVE NOOOOOOO - state: structuredClone(currBaseStore), - }) - // This mutates the state - this.options.transform?.fn(newObj) - const state = newObj.state - this.prevTransformArray = [...transformArray] - return state - } - - return currBaseStore - }, - }) - this.fieldMetaDerived = new Derived({ - deps: [this.mergedBaseStore], + deps: [this.baseStore], fn: ({ prevDepVals, currDepVals, prevVal: _prevVal }) => { const prevVal = _prevVal as | Record, AnyFieldMeta> @@ -1220,7 +1145,7 @@ export class FormApi< }) this.store = new Derived({ - deps: [this.mergedBaseStore, this.fieldMetaDerived], + deps: [this.baseStore, this.fieldMetaDerived], fn: ({ prevDepVals, currDepVals, prevVal: _prevVal }) => { const prevVal = _prevVal as | FormState< @@ -1451,11 +1376,9 @@ export class FormApi< } mount = () => { - const cleanupMergedBaseStoreDerived = this.mergedBaseStore.mount() const cleanupFieldMetaDerived = this.fieldMetaDerived.mount() const cleanupStoreDerived = this.store.mount() const cleanup = () => { - cleanupMergedBaseStoreDerived() cleanupFieldMetaDerived() cleanupStoreDerived() @@ -1510,11 +1433,6 @@ 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 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const shouldUpdateReeval = !!options.transform?.deps?.some( - (val, i) => val !== this.prevTransformArray[i], - ) - const shouldUpdateValues = options.defaultValues && !evaluate(options.defaultValues, oldOptions.defaultValues) && @@ -1524,16 +1442,7 @@ export class FormApi< !evaluate(options.defaultState, oldOptions.defaultState) && !this.state.isTouched - if (options.transform?.deps && !isTrackedDeps(options.transform.deps)) { - options.transform.deps = trackDeps(options.transform.deps, () => { - this.baseStore.setState((prevState) => ({ - ...prevState, - _force_re_eval: !(prevState._force_re_eval ?? false), - })) - }) - } - - if (!shouldUpdateValues && !shouldUpdateState && !shouldUpdateReeval) return + if (!shouldUpdateValues && !shouldUpdateState) return batch(() => { this.baseStore.setState(() => @@ -1549,10 +1458,6 @@ export class FormApi< values: options.defaultValues, } : {}, - - shouldUpdateReeval - ? { _force_re_eval: !this.state._force_re_eval } - : {}, ), ), ) @@ -2686,6 +2591,62 @@ export class FormApi< }) } + mergeAndUpdate = () => { + // Run the `transform` function on `this.state`, diff it, and update the relevant parts with what needs updating + if (!this.options.transform?.fn) return + + const newObj = Object.assign({}, this, { + // structuredClone is required to avoid `state` being mutated outside of this block + // Commonly available since 2022 in all major browsers BUT NOT REACT NATIVE NOOOOOOO + state: structuredClone(this.state), + }) + + this.options.transform.fn(newObj) + + if (newObj.fieldInfo !== this.fieldInfo) { + this.fieldInfo = newObj.fieldInfo + } + + if (newObj.options !== this.options) { + this.options = newObj.options + } + + const baseFormKeys = Object.keys({ + values: null, + validationMetaMap: null, + fieldMetaBase: null, + isSubmitting: null, + isSubmitted: null, + isValidating: null, + submissionAttempts: null, + isSubmitSuccessful: null, + _force_re_eval: null, + // Do not remove this, it ensures that we have all the keys in `BaseFormState` + } satisfies Record< + // Exclude errorMap since we need to handle that uniquely + Exclude, + null + >) as Array + + const diffedObject = baseFormKeys.reduce((prev, key) => { + if (this.state[key] !== newObj.state[key]) { + prev[key] = newObj.state[key] + } + return prev + }, {} as Partial) + + batch(() => { + if (Object.keys(diffedObject).length) { + this.baseStore.setState((prev) => ({ ...prev, ...diffedObject })) + } + + if (newObj.state.errorMap !== this.state.errorMap) { + // Check if we need to update `fieldMetaBase` with `errorMaps` set by + this.setErrorMap(newObj.state.errorMap) + } + }) + } + /** * Returns form and field level errors */ diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index cf98a8f54..422743969 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -4120,15 +4120,22 @@ describe('form api event client', () => { }) it('transform option does not invalidate state for the field', () => { - const state = { current: {} } as { current: Partial } + const state: Partial = { + errorMap: { + onChange: { + fields: { + age: 'Age is required', + } + } + } + } const form = new FormApi({ defaultValues: { age: 0, }, transform: { - fn: (f) => mergeForm(f as never, state.current) as never, - deps: [state.current], + fn: (f) => mergeForm(f as never, state) as never, }, }) @@ -4143,23 +4150,7 @@ it('transform option does not invalidate state for the field', () => { expect(ageField.state.meta.isValid).toBe(true) - state.current = { - errorMap: { - onServer: { - fields: { - age: 'Age is invalid from server', - }, - }, - }, - } - - console.log(form.mergedBaseStore.state.errorMap.onServer) - - form.options.transform!.deps[0] = state.current - - console.log(form.mergedBaseStore.state.errorMap.onServer) - - console.log(form.fieldMetaDerived) + form.mergeAndUpdate() expect(ageField.state.meta.isValid).toBe(false) }) From c70194a6b1479a2e8ddfd66ec30df703fec88748 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 1 Dec 2025 01:00:16 -0800 Subject: [PATCH 12/18] chore: demo working version --- .../next-server-actions/src/app/action.ts | 22 +++++++++---------- .../src/app/client-component.tsx | 8 +++++-- .../react-form-nextjs/src/useTransform.ts | 3 +-- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/examples/react/next-server-actions/src/app/action.ts b/examples/react/next-server-actions/src/app/action.ts index 2ccb9e7ab..a6a3b920f 100644 --- a/examples/react/next-server-actions/src/app/action.ts +++ b/examples/react/next-server-actions/src/app/action.ts @@ -1,26 +1,26 @@ -'use server'; +'use server' import { ServerValidateError, createServerValidate, -} from '@tanstack/react-form-nextjs'; -import { formOpts } from './shared-code'; -import { z } from 'zod'; +} from '@tanstack/react-form-nextjs' +import { formOpts } from './shared-code' +import { z } from 'zod' const schema = z.object({ - age: z.number().min(12), + age: z.coerce.number().min(12), firstName: z.string(), -}); +}) const serverValidate = createServerValidate({ ...formOpts, onServerValidate: schema, -}); +}) export default async function someAction(prev: unknown, formData: FormData) { try { - const validatedData = await serverValidate(formData); - console.log('validatedData', validatedData); + const validatedData = await serverValidate(formData) + console.log('validatedData', validatedData) // Persist the form data to the database // await sql` // INSERT INTO users (name, email, password) @@ -28,11 +28,11 @@ export default async function someAction(prev: unknown, formData: FormData) { // ` } catch (e) { if (e instanceof ServerValidateError) { - return e.formState; + return e.formState } // Some other error occurred while validating your form - throw e; + throw e } // Your form has successfully validated! diff --git a/examples/react/next-server-actions/src/app/client-component.tsx b/examples/react/next-server-actions/src/app/client-component.tsx index c1b687bd7..99d96642f 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useActionState } from 'react'; +import { useActionState, useLayoutEffect } from 'react' import { mergeForm, useForm } from '@tanstack/react-form-nextjs'; import { initialFormState, useTransform } from '@tanstack/react-form-nextjs'; import someAction from './action'; @@ -10,7 +10,7 @@ import { z } from 'zod'; export const ClientComp = () => { const [state, action] = useActionState(someAction, initialFormState); - debugger + // debugger const form = useForm({ ...formOpts, @@ -20,6 +20,10 @@ export const ClientComp = () => { ), }); + useLayoutEffect(() => { + form.mergeAndUpdate() + }, [state]) + return (
form.handleSubmit()}> AnyFormApi, - deps: unknown[], + _deps?: unknown[], ): FormTransform { return { fn, - deps, } } From 4bf648a56b77a8601c80870a95b59dc51622788a Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 1 Dec 2025 01:19:45 -0800 Subject: [PATCH 13/18] feat: move to useMerge away from useTransform --- .../src/app/client-component.tsx | 42 ++--- packages/form-core/src/FormApi.ts | 125 +------------- packages/form-core/src/index.ts | 1 + packages/form-core/src/mergeForm.ts | 67 +++++--- packages/form-core/src/transform.ts | 154 ++++++++++++++++++ packages/form-core/tests/FormApi.spec.ts | 36 ---- packages/form-core/tests/transform.spec.ts | 36 ++++ packages/react-form-nextjs/src/index.ts | 2 +- packages/react-form-nextjs/src/useMerge.ts | 88 ++++++++++ .../react-form-nextjs/src/useTransform.ts | 10 -- packages/react-form/src/index.ts | 1 + 11 files changed, 345 insertions(+), 217 deletions(-) create mode 100644 packages/form-core/src/transform.ts create mode 100644 packages/form-core/tests/transform.spec.ts create mode 100644 packages/react-form-nextjs/src/useMerge.ts delete mode 100644 packages/react-form-nextjs/src/useTransform.ts diff --git a/examples/react/next-server-actions/src/app/client-component.tsx b/examples/react/next-server-actions/src/app/client-component.tsx index 99d96642f..ef1f2d245 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -1,28 +1,28 @@ -'use client'; +'use client' -import { useActionState, useLayoutEffect } from 'react' -import { mergeForm, useForm } from '@tanstack/react-form-nextjs'; -import { initialFormState, useTransform } from '@tanstack/react-form-nextjs'; -import someAction from './action'; -import { formOpts } from './shared-code'; -import { z } from 'zod'; +import { useActionState } from 'react' +import { + initialFormState, + mergeForm, + useForm, + useMerge, +} from '@tanstack/react-form-nextjs' +import { z } from 'zod' +import someAction from './action' +import { formOpts } from './shared-code' export const ClientComp = () => { - const [state, action] = useActionState(someAction, initialFormState); - - // debugger + const [state, action] = useActionState(someAction, initialFormState) const form = useForm({ ...formOpts, - transform: useTransform( - (baseForm) => mergeForm(baseForm, state ?? {}), - [state] - ), - }); + }) - useLayoutEffect(() => { - form.mergeAndUpdate() - }, [state]) + useMerge({ + form, + fn: (baseForm) => mergeForm(baseForm, state ?? {}), + deps: [state], + }) return ( form.handleSubmit()}> @@ -45,7 +45,7 @@ export const ClientComp = () => {

{error?.message}

))} - ); + ) }}
{ )}
- ); -}; + ) +} diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 8c2603ae6..88696bf88 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -217,54 +217,6 @@ export interface FormValidators< onDynamicAsyncDebounceMs?: number } -/** - * @private - */ -export interface FormTransform< - TFormData, - TOnMount extends undefined | FormValidateOrFn, - TOnChange extends undefined | FormValidateOrFn, - TOnChangeAsync extends undefined | FormAsyncValidateOrFn, - TOnBlur extends undefined | FormValidateOrFn, - TOnBlurAsync extends undefined | FormAsyncValidateOrFn, - TOnSubmit extends undefined | FormValidateOrFn, - TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, - TOnDynamic extends undefined | FormValidateOrFn, - TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, - TOnServer extends undefined | FormAsyncValidateOrFn, - TSubmitMeta = never, -> { - fn: ( - formBase: FormApi< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - >, - ) => FormApi< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - > -} - export interface FormListeners< TFormData, TOnMount extends undefined | FormValidateOrFn, @@ -496,20 +448,6 @@ export interface FormOptions< > meta: TSubmitMeta }) => void - transform?: FormTransform< - NoInfer, - NoInfer, - NoInfer, - NoInfer, - NoInfer, - NoInfer, - NoInfer, - NoInfer, - NoInfer, - NoInfer, - NoInfer, - NoInfer - > } export type AnyFormOptions = FormOptions< @@ -657,7 +595,7 @@ export type BaseFormState< _force_re_eval?: boolean } -type AnyBaseFormState = BaseFormState< +export type AnyBaseFormState = BaseFormState< any, any, any, @@ -977,11 +915,6 @@ export class FormApi< return this.store.state } - /** - * @private - */ - prevTransformArray: unknown[] = [] - /** * @private */ @@ -2591,62 +2524,6 @@ export class FormApi< }) } - mergeAndUpdate = () => { - // Run the `transform` function on `this.state`, diff it, and update the relevant parts with what needs updating - if (!this.options.transform?.fn) return - - const newObj = Object.assign({}, this, { - // structuredClone is required to avoid `state` being mutated outside of this block - // Commonly available since 2022 in all major browsers BUT NOT REACT NATIVE NOOOOOOO - state: structuredClone(this.state), - }) - - this.options.transform.fn(newObj) - - if (newObj.fieldInfo !== this.fieldInfo) { - this.fieldInfo = newObj.fieldInfo - } - - if (newObj.options !== this.options) { - this.options = newObj.options - } - - const baseFormKeys = Object.keys({ - values: null, - validationMetaMap: null, - fieldMetaBase: null, - isSubmitting: null, - isSubmitted: null, - isValidating: null, - submissionAttempts: null, - isSubmitSuccessful: null, - _force_re_eval: null, - // Do not remove this, it ensures that we have all the keys in `BaseFormState` - } satisfies Record< - // Exclude errorMap since we need to handle that uniquely - Exclude, - null - >) as Array - - const diffedObject = baseFormKeys.reduce((prev, key) => { - if (this.state[key] !== newObj.state[key]) { - prev[key] = newObj.state[key] - } - return prev - }, {} as Partial) - - batch(() => { - if (Object.keys(diffedObject).length) { - this.baseStore.setState((prev) => ({ ...prev, ...diffedObject })) - } - - if (newObj.state.errorMap !== this.state.errorMap) { - // Check if we need to update `fieldMetaBase` with `errorMaps` set by - this.setErrorMap(newObj.state.errorMap) - } - }) - } - /** * Returns form and field level errors */ diff --git a/packages/form-core/src/index.ts b/packages/form-core/src/index.ts index 825eee5c6..94c0f3eea 100644 --- a/packages/form-core/src/index.ts +++ b/packages/form-core/src/index.ts @@ -9,3 +9,4 @@ export * from './standardSchemaValidator' export * from './FieldGroupApi' export * from './ValidationLogic' export * from './EventClient' +export * from './transform' diff --git a/packages/form-core/src/mergeForm.ts b/packages/form-core/src/mergeForm.ts index 9e43f0cab..e816e6754 100644 --- a/packages/form-core/src/mergeForm.ts +++ b/packages/form-core/src/mergeForm.ts @@ -1,4 +1,8 @@ -import type { FormApi } from './FormApi' +import type { + FormApi, + FormAsyncValidateOrFn, + FormValidateOrFn, +} from './FormApi' function isValidKey(key: string | number | symbol): boolean { const dangerousProps = ['__proto__', 'constructor', 'prototype'] @@ -70,35 +74,48 @@ export function mutateMergeDeep( return target } -export function mergeForm( +export function mergeForm< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta = never, +>( baseForm: FormApi< - NoInfer, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta >, state: Partial< FormApi< TFormData, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any, - any + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta >['state'] >, ) { diff --git a/packages/form-core/src/transform.ts b/packages/form-core/src/transform.ts new file mode 100644 index 000000000..9bd855d53 --- /dev/null +++ b/packages/form-core/src/transform.ts @@ -0,0 +1,154 @@ +import { batch } from '@tanstack/store' +import type { + AnyBaseFormState, + FormApi, + FormAsyncValidateOrFn, + FormValidateOrFn, +} from './FormApi' + +/** + * @private + */ +export type FormTransform< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta = never, +> = ( + formBase: FormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + >, +) => FormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta +> + +/** + * @private + */ +export function mergeAndUpdate< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta = never, +>( + form: FormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + >, + fn?: FormTransform< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + >, +) { + // Run the `transform` function on `form.state`, diff it, and update the relevant parts with what needs updating + if (!fn) return + + const newObj = Object.assign({}, form, { + // structuredClone is required to avoid `state` being mutated outside of this block + // Commonly available since 2022 in all major browsers BUT NOT REACT NATIVE NOOOOOOO + state: structuredClone(form.state), + }) + + fn(newObj) + + if (newObj.fieldInfo !== form.fieldInfo) { + form.fieldInfo = newObj.fieldInfo + } + + if (newObj.options !== form.options) { + form.options = newObj.options + } + + const baseFormKeys = Object.keys({ + values: null, + validationMetaMap: null, + fieldMetaBase: null, + isSubmitting: null, + isSubmitted: null, + isValidating: null, + submissionAttempts: null, + isSubmitSuccessful: null, + _force_re_eval: null, + // Do not remove this, it ensures that we have all the keys in `BaseFormState` + } satisfies Record< + // Exclude errorMap since we need to handle that uniquely + Exclude, + null + >) as Array + + const diffedObject = baseFormKeys.reduce((prev, key) => { + if (form.state[key] !== newObj.state[key]) { + prev[key] = newObj.state[key] + } + return prev + }, {} as Partial) + + batch(() => { + if (Object.keys(diffedObject).length) { + form.baseStore.setState((prev) => ({ ...prev, ...diffedObject })) + } + + if (newObj.state.errorMap !== form.state.errorMap) { + // Check if we need to update `fieldMetaBase` with `errorMaps` set by + form.setErrorMap(newObj.state.errorMap) + } + }) +} diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 422743969..9613f2cbb 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -4118,39 +4118,3 @@ describe('form api event client', () => { logSpy.mockRestore() }) }) - -it('transform option does not invalidate state for the field', () => { - const state: Partial = { - errorMap: { - onChange: { - fields: { - age: 'Age is required', - } - } - } - } - - const form = new FormApi({ - defaultValues: { - age: 0, - }, - transform: { - fn: (f) => mergeForm(f as never, state) as never, - }, - }) - - form.mount() - - const ageField = new FieldApi({ - form, - name: 'age', - }) - - ageField.mount() - - expect(ageField.state.meta.isValid).toBe(true) - - form.mergeAndUpdate() - - expect(ageField.state.meta.isValid).toBe(false) -}) diff --git a/packages/form-core/tests/transform.spec.ts b/packages/form-core/tests/transform.spec.ts new file mode 100644 index 000000000..f67b39352 --- /dev/null +++ b/packages/form-core/tests/transform.spec.ts @@ -0,0 +1,36 @@ +import { expect, it } from 'vitest' +import { FieldApi, FormApi, mergeAndUpdate, mergeForm } from '../src' +import type { AnyFormState } from '../src' + +it('transform option does not invalidate state for the field', () => { + const state: Partial = { + errorMap: { + onChange: { + fields: { + age: 'Age is required', + }, + }, + }, + } + + const form = new FormApi({ + defaultValues: { + age: 0, + }, + }) + + form.mount() + + const ageField = new FieldApi({ + form, + name: 'age', + }) + + ageField.mount() + + expect(ageField.state.meta.isValid).toBe(true) + + mergeAndUpdate(form, (f) => mergeForm(f, state)) + + expect(ageField.state.meta.isValid).toBe(false) +}) diff --git a/packages/react-form-nextjs/src/index.ts b/packages/react-form-nextjs/src/index.ts index cda540a02..b4beaeb8b 100644 --- a/packages/react-form-nextjs/src/index.ts +++ b/packages/react-form-nextjs/src/index.ts @@ -2,4 +2,4 @@ export * from '@tanstack/react-form' export * from './createServerValidate' export * from './error' -export * from './useTransform' +export * from './useMerge' diff --git a/packages/react-form-nextjs/src/useMerge.ts b/packages/react-form-nextjs/src/useMerge.ts new file mode 100644 index 000000000..605491322 --- /dev/null +++ b/packages/react-form-nextjs/src/useMerge.ts @@ -0,0 +1,88 @@ +import { mergeAndUpdate, useIsomorphicLayoutEffect } from '@tanstack/react-form' +import type { + FormApi, + FormAsyncValidateOrFn, + FormTransform, + FormValidateOrFn, +} from '@tanstack/react-form' + +interface UseMergeOptions< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta = never, +> { + form: FormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + > + fn?: FormTransform< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + > + deps: unknown[] +} + +export function useMerge< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta = never, +>({ + form, + fn, + deps, +}: UseMergeOptions< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta +>) { + useIsomorphicLayoutEffect(() => { + mergeAndUpdate(form, fn) + }, deps) +} diff --git a/packages/react-form-nextjs/src/useTransform.ts b/packages/react-form-nextjs/src/useTransform.ts deleted file mode 100644 index 5f3a47a29..000000000 --- a/packages/react-form-nextjs/src/useTransform.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { AnyFormApi, FormTransform } from '@tanstack/react-form' - -export function useTransform( - fn: (formBase: AnyFormApi) => AnyFormApi, - _deps?: unknown[], -): FormTransform { - return { - fn, - } -} diff --git a/packages/react-form/src/index.ts b/packages/react-form/src/index.ts index d58951cd2..e032da0e2 100644 --- a/packages/react-form/src/index.ts +++ b/packages/react-form/src/index.ts @@ -7,3 +7,4 @@ export * from './types' export * from './useField' export * from './useFieldGroup' export * from './useForm' +export * from './useIsomorphicLayoutEffect' From 106789f5ccec47b5d35820ae557dd8fbc4296452 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 1 Dec 2025 01:25:52 -0800 Subject: [PATCH 14/18] chore: fix remix and start --- .../react/remix/app/routes/_index/route.tsx | 13 +-- .../react/tanstack-start/src/routes/index.tsx | 9 +- packages/react-form-remix/src/index.ts | 2 +- packages/react-form-remix/src/useMerge.ts | 88 +++++++++++++++++++ packages/react-form-remix/src/useTransform.ts | 11 --- packages/react-form-start/src/index.ts | 2 +- packages/react-form-start/src/useMerge.ts | 88 +++++++++++++++++++ packages/react-form-start/src/useTransform.ts | 11 --- 8 files changed, 193 insertions(+), 31 deletions(-) create mode 100644 packages/react-form-remix/src/useMerge.ts delete mode 100644 packages/react-form-remix/src/useTransform.ts create mode 100644 packages/react-form-start/src/useMerge.ts delete mode 100644 packages/react-form-start/src/useTransform.ts diff --git a/examples/react/remix/app/routes/_index/route.tsx b/examples/react/remix/app/routes/_index/route.tsx index e754b6d0f..edcd82537 100644 --- a/examples/react/remix/app/routes/_index/route.tsx +++ b/examples/react/remix/app/routes/_index/route.tsx @@ -6,7 +6,7 @@ import { formOptions, mergeForm, useForm, - useTransform, + useMerge, } from '@tanstack/react-form-remix' import { useStore } from '@tanstack/react-store' @@ -56,11 +56,14 @@ export default function Index() { const form = useForm({ ...formOpts, - transform: useTransform( - (baseForm) => mergeForm(baseForm, actionData ?? {}), - [actionData], - ), }) + + useMerge({ + form, + fn: (baseForm) => mergeForm(baseForm, actionData ?? {}), + deps: [actionData], + }) + const formErrors = useStore(form.store, (formState) => formState.errors) return ( diff --git a/examples/react/tanstack-start/src/routes/index.tsx b/examples/react/tanstack-start/src/routes/index.tsx index af0b43b3b..208c8a79c 100644 --- a/examples/react/tanstack-start/src/routes/index.tsx +++ b/examples/react/tanstack-start/src/routes/index.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { mergeForm, useForm, useTransform } from '@tanstack/react-form-start' +import { mergeForm, useForm, useMerge } from '@tanstack/react-form-start' import { useStore } from '@tanstack/react-store' import { getFormDataFromServer, handleForm } from 'src/utils/form' import { formOpts } from 'src/utils/form-isomorphic' @@ -16,7 +16,12 @@ function Home() { const form = useForm({ ...formOpts, - transform: useTransform((baseForm) => mergeForm(baseForm, state), [state]), + }) + + useMerge({ + form, + fn: (baseForm) => mergeForm(baseForm, state), + deps: [state], }) const formErrors = useStore(form.store, (formState) => formState.errors) diff --git a/packages/react-form-remix/src/index.ts b/packages/react-form-remix/src/index.ts index cda540a02..b4beaeb8b 100644 --- a/packages/react-form-remix/src/index.ts +++ b/packages/react-form-remix/src/index.ts @@ -2,4 +2,4 @@ export * from '@tanstack/react-form' export * from './createServerValidate' export * from './error' -export * from './useTransform' +export * from './useMerge' diff --git a/packages/react-form-remix/src/useMerge.ts b/packages/react-form-remix/src/useMerge.ts new file mode 100644 index 000000000..605491322 --- /dev/null +++ b/packages/react-form-remix/src/useMerge.ts @@ -0,0 +1,88 @@ +import { mergeAndUpdate, useIsomorphicLayoutEffect } from '@tanstack/react-form' +import type { + FormApi, + FormAsyncValidateOrFn, + FormTransform, + FormValidateOrFn, +} from '@tanstack/react-form' + +interface UseMergeOptions< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta = never, +> { + form: FormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + > + fn?: FormTransform< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + > + deps: unknown[] +} + +export function useMerge< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta = never, +>({ + form, + fn, + deps, +}: UseMergeOptions< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta +>) { + useIsomorphicLayoutEffect(() => { + mergeAndUpdate(form, fn) + }, deps) +} diff --git a/packages/react-form-remix/src/useTransform.ts b/packages/react-form-remix/src/useTransform.ts deleted file mode 100644 index 9bb9abfc8..000000000 --- a/packages/react-form-remix/src/useTransform.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { AnyFormApi, FormTransform } from '@tanstack/react-form' - -export function useTransform( - fn: (formBase: AnyFormApi) => AnyFormApi, - deps: unknown[], -): FormTransform { - return { - fn, - deps, - } -} diff --git a/packages/react-form-start/src/index.ts b/packages/react-form-start/src/index.ts index d7b045a2e..8db8dce55 100644 --- a/packages/react-form-start/src/index.ts +++ b/packages/react-form-start/src/index.ts @@ -3,4 +3,4 @@ export * from '@tanstack/react-form' export * from './createServerValidate' export * from './getFormData' export * from './error' -export * from './useTransform' +export * from './useMerge' diff --git a/packages/react-form-start/src/useMerge.ts b/packages/react-form-start/src/useMerge.ts new file mode 100644 index 000000000..605491322 --- /dev/null +++ b/packages/react-form-start/src/useMerge.ts @@ -0,0 +1,88 @@ +import { mergeAndUpdate, useIsomorphicLayoutEffect } from '@tanstack/react-form' +import type { + FormApi, + FormAsyncValidateOrFn, + FormTransform, + FormValidateOrFn, +} from '@tanstack/react-form' + +interface UseMergeOptions< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta = never, +> { + form: FormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + > + fn?: FormTransform< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + > + deps: unknown[] +} + +export function useMerge< + TFormData, + TOnMount extends undefined | FormValidateOrFn, + TOnChange extends undefined | FormValidateOrFn, + TOnChangeAsync extends undefined | FormAsyncValidateOrFn, + TOnBlur extends undefined | FormValidateOrFn, + TOnBlurAsync extends undefined | FormAsyncValidateOrFn, + TOnSubmit extends undefined | FormValidateOrFn, + TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, + TOnDynamic extends undefined | FormValidateOrFn, + TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, + TOnServer extends undefined | FormAsyncValidateOrFn, + TSubmitMeta = never, +>({ + form, + fn, + deps, +}: UseMergeOptions< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta +>) { + useIsomorphicLayoutEffect(() => { + mergeAndUpdate(form, fn) + }, deps) +} diff --git a/packages/react-form-start/src/useTransform.ts b/packages/react-form-start/src/useTransform.ts deleted file mode 100644 index 9bb9abfc8..000000000 --- a/packages/react-form-start/src/useTransform.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { AnyFormApi, FormTransform } from '@tanstack/react-form' - -export function useTransform( - fn: (formBase: AnyFormApi) => AnyFormApi, - deps: unknown[], -): FormTransform { - return { - fn, - deps, - } -} From b91b88dbb544459f605c8193a5c2a76b1787937c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 09:31:13 +0000 Subject: [PATCH 15/18] ci: apply automated fixes and generate docs --- examples/react/next-server-actions/tsconfig.json | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/examples/react/next-server-actions/tsconfig.json b/examples/react/next-server-actions/tsconfig.json index 5ba546ad3..4c2b4986c 100644 --- a/examples/react/next-server-actions/tsconfig.json +++ b/examples/react/next-server-actions/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "DOM", - "DOM.Iterable", - "ESNext" - ], + "lib": ["DOM", "DOM.Iterable", "ESNext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -30,7 +26,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } From 88d0d7c01d59a9aecae148b14e39cdf2654b3205 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 8 Dec 2025 14:16:37 -0800 Subject: [PATCH 16/18] chore: attempt 8 --- .../next-server-actions/src/app/action.ts | 2 +- .../src/app/client-component.tsx | 16 ++-- packages/form-core/src/transform.ts | 2 + packages/react-form-nextjs/src/index.ts | 2 +- packages/react-form-nextjs/src/useMerge.ts | 88 ------------------- .../react-form-nextjs/src/useTransform.ts | 7 ++ packages/react-form/src/useForm.tsx | 18 +++- 7 files changed, 34 insertions(+), 101 deletions(-) delete mode 100644 packages/react-form-nextjs/src/useMerge.ts create mode 100644 packages/react-form-nextjs/src/useTransform.ts diff --git a/examples/react/next-server-actions/src/app/action.ts b/examples/react/next-server-actions/src/app/action.ts index a6a3b920f..76ce9e669 100644 --- a/examples/react/next-server-actions/src/app/action.ts +++ b/examples/react/next-server-actions/src/app/action.ts @@ -4,8 +4,8 @@ import { ServerValidateError, createServerValidate, } from '@tanstack/react-form-nextjs' -import { formOpts } from './shared-code' import { z } from 'zod' +import { formOpts } from './shared-code' const schema = z.object({ age: z.coerce.number().min(12), diff --git a/examples/react/next-server-actions/src/app/client-component.tsx b/examples/react/next-server-actions/src/app/client-component.tsx index ef1f2d245..00ec03e06 100644 --- a/examples/react/next-server-actions/src/app/client-component.tsx +++ b/examples/react/next-server-actions/src/app/client-component.tsx @@ -1,11 +1,11 @@ 'use client' -import { useActionState } from 'react' +import { useActionState, useCallback } from 'react' import { initialFormState, mergeForm, useForm, - useMerge, + useTransform, } from '@tanstack/react-form-nextjs' import { z } from 'zod' import someAction from './action' @@ -14,14 +14,14 @@ import { formOpts } from './shared-code' export const ClientComp = () => { const [state, action] = useActionState(someAction, initialFormState) + // debugger + const form = useForm({ ...formOpts, - }) - - useMerge({ - form, - fn: (baseForm) => mergeForm(baseForm, state ?? {}), - deps: [state], + transform: useCallback( + (baseForm: unknown) => mergeForm(baseForm as never, state ?? {}), + [state], + ), }) return ( diff --git a/packages/form-core/src/transform.ts b/packages/form-core/src/transform.ts index 9bd855d53..10d09da49 100644 --- a/packages/form-core/src/transform.ts +++ b/packages/form-core/src/transform.ts @@ -151,4 +151,6 @@ export function mergeAndUpdate< form.setErrorMap(newObj.state.errorMap) } }) + + return newObj; } diff --git a/packages/react-form-nextjs/src/index.ts b/packages/react-form-nextjs/src/index.ts index b4beaeb8b..cda540a02 100644 --- a/packages/react-form-nextjs/src/index.ts +++ b/packages/react-form-nextjs/src/index.ts @@ -2,4 +2,4 @@ export * from '@tanstack/react-form' export * from './createServerValidate' export * from './error' -export * from './useMerge' +export * from './useTransform' diff --git a/packages/react-form-nextjs/src/useMerge.ts b/packages/react-form-nextjs/src/useMerge.ts deleted file mode 100644 index 605491322..000000000 --- a/packages/react-form-nextjs/src/useMerge.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { mergeAndUpdate, useIsomorphicLayoutEffect } from '@tanstack/react-form' -import type { - FormApi, - FormAsyncValidateOrFn, - FormTransform, - FormValidateOrFn, -} from '@tanstack/react-form' - -interface UseMergeOptions< - TFormData, - TOnMount extends undefined | FormValidateOrFn, - TOnChange extends undefined | FormValidateOrFn, - TOnChangeAsync extends undefined | FormAsyncValidateOrFn, - TOnBlur extends undefined | FormValidateOrFn, - TOnBlurAsync extends undefined | FormAsyncValidateOrFn, - TOnSubmit extends undefined | FormValidateOrFn, - TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, - TOnDynamic extends undefined | FormValidateOrFn, - TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, - TOnServer extends undefined | FormAsyncValidateOrFn, - TSubmitMeta = never, -> { - form: FormApi< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - > - fn?: FormTransform< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - > - deps: unknown[] -} - -export function useMerge< - TFormData, - TOnMount extends undefined | FormValidateOrFn, - TOnChange extends undefined | FormValidateOrFn, - TOnChangeAsync extends undefined | FormAsyncValidateOrFn, - TOnBlur extends undefined | FormValidateOrFn, - TOnBlurAsync extends undefined | FormAsyncValidateOrFn, - TOnSubmit extends undefined | FormValidateOrFn, - TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, - TOnDynamic extends undefined | FormValidateOrFn, - TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, - TOnServer extends undefined | FormAsyncValidateOrFn, - TSubmitMeta = never, ->({ - form, - fn, - deps, -}: UseMergeOptions< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta ->) { - useIsomorphicLayoutEffect(() => { - mergeAndUpdate(form, fn) - }, deps) -} diff --git a/packages/react-form-nextjs/src/useTransform.ts b/packages/react-form-nextjs/src/useTransform.ts new file mode 100644 index 000000000..d333603c6 --- /dev/null +++ b/packages/react-form-nextjs/src/useTransform.ts @@ -0,0 +1,7 @@ +import { useCallback } from 'react' +import type { AnyFormApi } from '@tanstack/react-form' + +export const useTransform: ( + fn: (formBase: AnyFormApi) => AnyFormApi, + deps?: unknown[], +) => unknown = useCallback as never diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 5abaa0351..2b06bf7df 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -1,6 +1,6 @@ 'use client' -import { FormApi, functionalUpdate } from '@tanstack/form-core' +import { FormApi, functionalUpdate, mergeAndUpdate } from '@tanstack/form-core' import { useStore } from '@tanstack/react-store' import { useMemo, useState } from 'react' import { Field } from './useField' @@ -183,7 +183,10 @@ export function useForm< TOnDynamicAsync, TOnServer, TSubmitMeta - >, + > & { + // Made stable by `useTransform` + transform?: (data: unknown) => unknown + }, ) { const fallbackFormId = useFormId() const [prevFormId, setPrevFormId] = useState(opts?.formId as never) @@ -211,6 +214,8 @@ export function useForm< setPrevFormId(formId) } + const transform = useMemo(() => opts?.transform, [opts?.transform]) + const extendedFormApi = useMemo(() => { const extendedApi: ReactFormExtendedApi< TFormData, @@ -239,6 +244,13 @@ export function useForm< }, } as never + if (transform) { + // Cannot call synchronously, as otherwise we're setting state mid-render, which breaks React + setTimeout(() => { + mergeAndUpdate(extendedApi, transform as never) + }, 0) + } + extendedApi.Field = function APIField(props) { return } @@ -254,7 +266,7 @@ export function useForm< } return extendedApi - }, [formApi]) + }, [formApi, transform]) useIsomorphicLayoutEffect(formApi.mount, []) From 24468580518f19dda495d4a2eeff12e24336b06a Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Mon, 8 Dec 2025 14:19:22 -0800 Subject: [PATCH 17/18] chore: attempt to fix Start and Remix as well --- .../react/remix/app/routes/_index/route.tsx | 13 ++- .../react/tanstack-start/src/routes/index.tsx | 9 +- packages/react-form-remix/src/index.ts | 2 +- packages/react-form-remix/src/useMerge.ts | 88 ------------------- packages/react-form-remix/src/useTransform.ts | 7 ++ packages/react-form-start/src/index.ts | 2 +- packages/react-form-start/src/useMerge.ts | 88 ------------------- packages/react-form-start/src/useTransform.ts | 7 ++ 8 files changed, 23 insertions(+), 193 deletions(-) delete mode 100644 packages/react-form-remix/src/useMerge.ts create mode 100644 packages/react-form-remix/src/useTransform.ts delete mode 100644 packages/react-form-start/src/useMerge.ts create mode 100644 packages/react-form-start/src/useTransform.ts diff --git a/examples/react/remix/app/routes/_index/route.tsx b/examples/react/remix/app/routes/_index/route.tsx index edcd82537..e754b6d0f 100644 --- a/examples/react/remix/app/routes/_index/route.tsx +++ b/examples/react/remix/app/routes/_index/route.tsx @@ -6,7 +6,7 @@ import { formOptions, mergeForm, useForm, - useMerge, + useTransform, } from '@tanstack/react-form-remix' import { useStore } from '@tanstack/react-store' @@ -56,14 +56,11 @@ export default function Index() { const form = useForm({ ...formOpts, + transform: useTransform( + (baseForm) => mergeForm(baseForm, actionData ?? {}), + [actionData], + ), }) - - useMerge({ - form, - fn: (baseForm) => mergeForm(baseForm, actionData ?? {}), - deps: [actionData], - }) - const formErrors = useStore(form.store, (formState) => formState.errors) return ( diff --git a/examples/react/tanstack-start/src/routes/index.tsx b/examples/react/tanstack-start/src/routes/index.tsx index 208c8a79c..af0b43b3b 100644 --- a/examples/react/tanstack-start/src/routes/index.tsx +++ b/examples/react/tanstack-start/src/routes/index.tsx @@ -1,5 +1,5 @@ import { createFileRoute } from '@tanstack/react-router' -import { mergeForm, useForm, useMerge } from '@tanstack/react-form-start' +import { mergeForm, useForm, useTransform } from '@tanstack/react-form-start' import { useStore } from '@tanstack/react-store' import { getFormDataFromServer, handleForm } from 'src/utils/form' import { formOpts } from 'src/utils/form-isomorphic' @@ -16,12 +16,7 @@ function Home() { const form = useForm({ ...formOpts, - }) - - useMerge({ - form, - fn: (baseForm) => mergeForm(baseForm, state), - deps: [state], + transform: useTransform((baseForm) => mergeForm(baseForm, state), [state]), }) const formErrors = useStore(form.store, (formState) => formState.errors) diff --git a/packages/react-form-remix/src/index.ts b/packages/react-form-remix/src/index.ts index b4beaeb8b..cda540a02 100644 --- a/packages/react-form-remix/src/index.ts +++ b/packages/react-form-remix/src/index.ts @@ -2,4 +2,4 @@ export * from '@tanstack/react-form' export * from './createServerValidate' export * from './error' -export * from './useMerge' +export * from './useTransform' diff --git a/packages/react-form-remix/src/useMerge.ts b/packages/react-form-remix/src/useMerge.ts deleted file mode 100644 index 605491322..000000000 --- a/packages/react-form-remix/src/useMerge.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { mergeAndUpdate, useIsomorphicLayoutEffect } from '@tanstack/react-form' -import type { - FormApi, - FormAsyncValidateOrFn, - FormTransform, - FormValidateOrFn, -} from '@tanstack/react-form' - -interface UseMergeOptions< - TFormData, - TOnMount extends undefined | FormValidateOrFn, - TOnChange extends undefined | FormValidateOrFn, - TOnChangeAsync extends undefined | FormAsyncValidateOrFn, - TOnBlur extends undefined | FormValidateOrFn, - TOnBlurAsync extends undefined | FormAsyncValidateOrFn, - TOnSubmit extends undefined | FormValidateOrFn, - TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, - TOnDynamic extends undefined | FormValidateOrFn, - TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, - TOnServer extends undefined | FormAsyncValidateOrFn, - TSubmitMeta = never, -> { - form: FormApi< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - > - fn?: FormTransform< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - > - deps: unknown[] -} - -export function useMerge< - TFormData, - TOnMount extends undefined | FormValidateOrFn, - TOnChange extends undefined | FormValidateOrFn, - TOnChangeAsync extends undefined | FormAsyncValidateOrFn, - TOnBlur extends undefined | FormValidateOrFn, - TOnBlurAsync extends undefined | FormAsyncValidateOrFn, - TOnSubmit extends undefined | FormValidateOrFn, - TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, - TOnDynamic extends undefined | FormValidateOrFn, - TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, - TOnServer extends undefined | FormAsyncValidateOrFn, - TSubmitMeta = never, ->({ - form, - fn, - deps, -}: UseMergeOptions< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta ->) { - useIsomorphicLayoutEffect(() => { - mergeAndUpdate(form, fn) - }, deps) -} diff --git a/packages/react-form-remix/src/useTransform.ts b/packages/react-form-remix/src/useTransform.ts new file mode 100644 index 000000000..d333603c6 --- /dev/null +++ b/packages/react-form-remix/src/useTransform.ts @@ -0,0 +1,7 @@ +import { useCallback } from 'react' +import type { AnyFormApi } from '@tanstack/react-form' + +export const useTransform: ( + fn: (formBase: AnyFormApi) => AnyFormApi, + deps?: unknown[], +) => unknown = useCallback as never diff --git a/packages/react-form-start/src/index.ts b/packages/react-form-start/src/index.ts index 8db8dce55..d7b045a2e 100644 --- a/packages/react-form-start/src/index.ts +++ b/packages/react-form-start/src/index.ts @@ -3,4 +3,4 @@ export * from '@tanstack/react-form' export * from './createServerValidate' export * from './getFormData' export * from './error' -export * from './useMerge' +export * from './useTransform' diff --git a/packages/react-form-start/src/useMerge.ts b/packages/react-form-start/src/useMerge.ts deleted file mode 100644 index 605491322..000000000 --- a/packages/react-form-start/src/useMerge.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { mergeAndUpdate, useIsomorphicLayoutEffect } from '@tanstack/react-form' -import type { - FormApi, - FormAsyncValidateOrFn, - FormTransform, - FormValidateOrFn, -} from '@tanstack/react-form' - -interface UseMergeOptions< - TFormData, - TOnMount extends undefined | FormValidateOrFn, - TOnChange extends undefined | FormValidateOrFn, - TOnChangeAsync extends undefined | FormAsyncValidateOrFn, - TOnBlur extends undefined | FormValidateOrFn, - TOnBlurAsync extends undefined | FormAsyncValidateOrFn, - TOnSubmit extends undefined | FormValidateOrFn, - TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, - TOnDynamic extends undefined | FormValidateOrFn, - TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, - TOnServer extends undefined | FormAsyncValidateOrFn, - TSubmitMeta = never, -> { - form: FormApi< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - > - fn?: FormTransform< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta - > - deps: unknown[] -} - -export function useMerge< - TFormData, - TOnMount extends undefined | FormValidateOrFn, - TOnChange extends undefined | FormValidateOrFn, - TOnChangeAsync extends undefined | FormAsyncValidateOrFn, - TOnBlur extends undefined | FormValidateOrFn, - TOnBlurAsync extends undefined | FormAsyncValidateOrFn, - TOnSubmit extends undefined | FormValidateOrFn, - TOnSubmitAsync extends undefined | FormAsyncValidateOrFn, - TOnDynamic extends undefined | FormValidateOrFn, - TOnDynamicAsync extends undefined | FormAsyncValidateOrFn, - TOnServer extends undefined | FormAsyncValidateOrFn, - TSubmitMeta = never, ->({ - form, - fn, - deps, -}: UseMergeOptions< - TFormData, - TOnMount, - TOnChange, - TOnChangeAsync, - TOnBlur, - TOnBlurAsync, - TOnSubmit, - TOnSubmitAsync, - TOnDynamic, - TOnDynamicAsync, - TOnServer, - TSubmitMeta ->) { - useIsomorphicLayoutEffect(() => { - mergeAndUpdate(form, fn) - }, deps) -} diff --git a/packages/react-form-start/src/useTransform.ts b/packages/react-form-start/src/useTransform.ts new file mode 100644 index 000000000..d333603c6 --- /dev/null +++ b/packages/react-form-start/src/useTransform.ts @@ -0,0 +1,7 @@ +import { useCallback } from 'react' +import type { AnyFormApi } from '@tanstack/react-form' + +export const useTransform: ( + fn: (formBase: AnyFormApi) => AnyFormApi, + deps?: unknown[], +) => unknown = useCallback as never From 322979ecf11aa42274fe7fa858542e2d48f3b648 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:20:10 +0000 Subject: [PATCH 18/18] ci: apply automated fixes and generate docs --- packages/form-core/src/transform.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/form-core/src/transform.ts b/packages/form-core/src/transform.ts index 10d09da49..d8014c351 100644 --- a/packages/form-core/src/transform.ts +++ b/packages/form-core/src/transform.ts @@ -152,5 +152,5 @@ export function mergeAndUpdate< } }) - return newObj; + return newObj }