diff --git a/.changeset/issue-1980-fix.md b/.changeset/issue-1980-fix.md new file mode 100644 index 000000000..8a701bb0b --- /dev/null +++ b/.changeset/issue-1980-fix.md @@ -0,0 +1,6 @@ +--- +'@tanstack/react-form': patch +'@tanstack/form-core': patch +--- + +fix: subscribe to full meta object in useField to support custom meta properties diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index ff52bbe74..d8b6a909a 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2270,16 +2270,24 @@ export class FormApi< batch(() => { if (!dontUpdateMeta) { - this.setFieldMeta(field, (prev) => ({ - ...prev, - isTouched: true, - isDirty: true, - errorMap: { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - ...prev?.errorMap, - onMount: undefined, - }, - })) + const meta = this.getFieldMeta(field) + + if ( + !meta?.isTouched || + !meta.isDirty || + meta.errorMap.onMount !== undefined + ) { + this.setFieldMeta(field, (prev) => ({ + ...prev, + isTouched: true, + isDirty: true, + errorMap: { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + ...prev?.errorMap, + onMount: undefined, + }, + })) + } } this.baseStore.setState((prev) => { diff --git a/packages/react-form/src/useField.tsx b/packages/react-form/src/useField.tsx index 2a3f546cf..643ef59ac 100644 --- a/packages/react-form/src/useField.tsx +++ b/packages/react-form/src/useField.tsx @@ -231,30 +231,8 @@ export function useField< state: typeof fieldApi.state, ) => TData | number, ) - const reactiveMetaIsTouched = useStore( - fieldApi.store, - (state) => state.meta.isTouched, - ) - const reactiveMetaIsBlurred = useStore( - fieldApi.store, - (state) => state.meta.isBlurred, - ) - const reactiveMetaIsDirty = useStore( - fieldApi.store, - (state) => state.meta.isDirty, - ) - const reactiveMetaErrorMap = useStore( - fieldApi.store, - (state) => state.meta.errorMap, - ) - const reactiveMetaErrorSourceMap = useStore( - fieldApi.store, - (state) => state.meta.errorSourceMap, - ) - const reactiveMetaIsValidating = useStore( - fieldApi.store, - (state) => state.meta.isValidating, - ) + + const reactiveMeta = useStore(fieldApi.store, (state) => state.meta) // This makes me sad, but if I understand correctly, this is what we have to do for reactivity to work properly with React compiler. const extendedFieldApi = useMemo(() => { @@ -266,17 +244,7 @@ export function useField< // so we need to get the actual value from fieldApi value: opts.mode === 'array' ? fieldApi.state.value : reactiveStateValue, - get meta() { - return { - ...fieldApi.state.meta, - isTouched: reactiveMetaIsTouched, - isBlurred: reactiveMetaIsBlurred, - isDirty: reactiveMetaIsDirty, - errorMap: reactiveMetaErrorMap, - errorSourceMap: reactiveMetaErrorSourceMap, - isValidating: reactiveMetaIsValidating, - } satisfies AnyFieldMeta - }, + meta: reactiveMeta, } satisfies AnyFieldApi['state'] }, } @@ -324,17 +292,7 @@ export function useField< extendedApi.Field = Field as never return extendedApi - }, [ - fieldApi, - opts.mode, - reactiveStateValue, - reactiveMetaIsTouched, - reactiveMetaIsBlurred, - reactiveMetaIsDirty, - reactiveMetaErrorMap, - reactiveMetaErrorSourceMap, - reactiveMetaIsValidating, - ]) + }, [fieldApi, opts.mode, reactiveStateValue, reactiveMeta]) useIsomorphicLayoutEffect(fieldApi.mount, [fieldApi]) diff --git a/packages/react-form/tests/issue-1980.test.tsx b/packages/react-form/tests/issue-1980.test.tsx new file mode 100644 index 000000000..083776810 --- /dev/null +++ b/packages/react-form/tests/issue-1980.test.tsx @@ -0,0 +1,130 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import React from 'react' +import { expect, test } from 'vitest' +import { useForm } from '../src/index' + +function SimpleForm() { + const form = useForm({ + defaultValues: { + firstName: '', + color: 'red', + }, + onSubmit: async ({ value }) => { + // Do something with form data + console.log(value) + }, + }) + + return ( +