diff --git a/packages/form-core/src/FormApi/FormApi.lib.ts b/packages/form-core/src/FormApi/FormApi.lib.ts index 8a8575788..08c6681b2 100644 --- a/packages/form-core/src/FormApi/FormApi.lib.ts +++ b/packages/form-core/src/FormApi/FormApi.lib.ts @@ -502,7 +502,22 @@ export class InternalFormApi< updateOptions.fieldApiOverride = field batch(() => { - this._atoms.values.set((prev) => setBy(prev, fieldName, updater)) + const previousValue = this.getFieldValue(fieldName) + const nextValue = callUpdater(updater, previousValue) + const replacedSameLengthArray = + Array.isArray(previousValue) && + Array.isArray(nextValue) && + previousValue !== nextValue && + previousValue.length === nextValue.length + + this._atoms.values.set((prev) => setBy(prev, fieldName, nextValue)) + + if (field && replacedSameLengthArray) { + field._setMeta((prev) => ({ + ...prev, + _arrayVersion: prev._arrayVersion + 1, + })) + } this._notifyFieldChange(field, updateOptions) }) diff --git a/packages/form-core/src/FormApi/array-methods.lib.ts b/packages/form-core/src/FormApi/array-methods.lib.ts index ea568f8c2..55c86da77 100644 --- a/packages/form-core/src/FormApi/array-methods.lib.ts +++ b/packages/form-core/src/FormApi/array-methods.lib.ts @@ -203,12 +203,6 @@ function swapFieldValues({ if (!arrayField) return - // Since the length wasn't changed, we need to notify manually - arrayField._setMeta((prev) => ({ - ...prev, - _arrayVersion: prev._arrayVersion + 1, - })) - const fieldA = tryGetFieldApi(arrayField, [indexA]) const fieldB = tryGetFieldApi(arrayField, [indexB]) @@ -266,11 +260,6 @@ function moveFieldValue({ if (!arrayField) return - arrayField._setMeta((prev) => ({ - ...prev, - _arrayVersion: prev._arrayVersion + 1, - })) - const movingChild = tryGetFieldApi(arrayField, [fromIndex]) for (const child of arrayField._children) { @@ -312,11 +301,6 @@ function clearFieldValues({ if (!arrayField) return - arrayField._setMeta((prev) => ({ - ...prev, - _arrayVersion: prev._arrayVersion + 1, - })) - // Kill all child fields since the array is now empty // _kill() will remove each child from the parent's children for (const child of arrayField._children) { diff --git a/packages/form-core/tests/FormApi/field-state.spec.ts b/packages/form-core/tests/FormApi/field-state.spec.ts index 3570952ee..970c6e65c 100644 --- a/packages/form-core/tests/FormApi/field-state.spec.ts +++ b/packages/form-core/tests/FormApi/field-state.spec.ts @@ -55,6 +55,59 @@ describe('form - field state', () => { expect(form.getFieldValue('count')).toBe(2) }) + it('increments the array version for a same-length array replacement', () => { + const form = new InternalFormApi({ defaultValues: { items: ['a'] } }) + const field = form._getOrCreateFieldApi({ name: 'items' }) + + form.setFieldValue('items', ['b']) + + expect(form.getFieldValue('items')).toEqual(['b']) + expect(field.meta._arrayVersion).toBe(1) + }) + + it('increments the array version for an updater replacement', () => { + const form = new InternalFormApi({ defaultValues: { items: ['a'] } }) + const field = form._getOrCreateFieldApi({ name: 'items' }) + + form.setFieldValue('items', (items: Array) => + items.map((item) => item.toUpperCase()), + ) + + expect(form.getFieldValue('items')).toEqual(['A']) + expect(field.meta._arrayVersion).toBe(1) + }) + + it('does not increment a parent array version for a nested field update', () => { + const form = new InternalFormApi({ + defaultValues: { items: [{ label: 'a' }] }, + }) + const field = form._getOrCreateFieldApi({ name: 'items' }) + + form.setFieldValue('items[0].label', 'b') + + expect(form.getFieldValue('items')).toEqual([{ label: 'b' }]) + expect(field.meta._arrayVersion).toBe(0) + }) + + it('increments the array version only once for array helpers', () => { + const form = new InternalFormApi({ + defaultValues: { items: ['a', 'b', 'c'] }, + }) + const field = form._getOrCreateFieldApi({ name: 'items' }) + + form.swapFieldValues('items', 0, 1) + expect(field.meta._arrayVersion).toBe(1) + + form.moveFieldValue('items', 0, 2) + expect(field.meta._arrayVersion).toBe(2) + + form.clearFieldValues('items') + expect(field.meta._arrayVersion).toBe(2) + + form.clearFieldValues('items') + expect(field.meta._arrayVersion).toBe(3) + }) + it('marks form isTouched and isDirty after a change', () => { const form = new InternalFormApi({ defaultValues: { name: '' } }) const field = form._getOrCreateFieldApi({ name: 'name' }) diff --git a/packages/react-form/tests/FormField.spec.tsx b/packages/react-form/tests/FormField.spec.tsx index badfe76f7..180d05c05 100644 --- a/packages/react-form/tests/FormField.spec.tsx +++ b/packages/react-form/tests/FormField.spec.tsx @@ -101,6 +101,53 @@ describe('Form fields', () => { expect(input).toHaveValue('new-value') }) + it('rerenders an ArrayField for a same-length replacement', () => { + let renders = 0 + + function Component() { + const form = useForm({ + defaultValues: { items: [{ label: 'A' }] }, + }) + + return ( + <> +