From 429d060f32aceaceaca8d27db45e21061c2896be Mon Sep 17 00:00:00 2001 From: KooYS <17160263+KooYS@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:35:20 +0900 Subject: [PATCH] fix(form-core): set array values at the given index past the end setBy only padded the array when the slice before the index was empty, so writing past the end of a non-empty array placed the value at `length` instead of at the index. Pad the gap with undefined, which is what already happens for empty arrays. --- .changeset/quiet-slots-stay.md | 5 +++ packages/form-core/src/utils.lib.ts | 3 +- .../tests/FormApi/FormApi.lib.spec.ts | 42 +++++++++++++++++++ packages/form-core/tests/utils.test.ts | 6 ++- 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 .changeset/quiet-slots-stay.md diff --git a/.changeset/quiet-slots-stay.md b/.changeset/quiet-slots-stay.md new file mode 100644 index 0000000000..5640ec3a80 --- /dev/null +++ b/.changeset/quiet-slots-stay.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': patch +--- + +Fix setting an array value at an index past the end of a non-empty array writing to the wrong index diff --git a/packages/form-core/src/utils.lib.ts b/packages/form-core/src/utils.lib.ts index d1e0005cb0..e1325be667 100644 --- a/packages/form-core/src/utils.lib.ts +++ b/packages/form-core/src/utils.lib.ts @@ -181,7 +181,8 @@ export function setBy(obj: any, _path: any, updater: Updater) { if (Array.isArray(parent) && typeof key === 'number') { const prefix = parent.slice(0, key) return [ - ...(prefix.length ? prefix : new Array(key)), + ...prefix, + ...new Array(key - prefix.length), doSet(parent[key]), ...parent.slice(key + 1), ] diff --git a/packages/form-core/tests/FormApi/FormApi.lib.spec.ts b/packages/form-core/tests/FormApi/FormApi.lib.spec.ts index 3dc9393ff7..2aa39932f1 100644 --- a/packages/form-core/tests/FormApi/FormApi.lib.spec.ts +++ b/packages/form-core/tests/FormApi/FormApi.lib.spec.ts @@ -742,6 +742,33 @@ describe('InternalFormApi - setFieldValue', () => { expect(field.value).toEqual(['B']) expect(form.defaultValues).toEqual({ items: ['a'] }) }) + it('sets a value at an index past the end of a non-empty array', () => { + const form = new InternalFormApi({ + defaultValues: { phones: ['555-0000'] }, + }) + const fields = [0, 1, 2, 3].map((i) => + form._getOrCreateFieldApi({ name: `phones[${i}]` as const }), + ) + + fields[3]!.handleChange('555-0003') + + expect(form.getFieldValue('phones[3]')).toBe('555-0003') + expect(form.state.values.phones).toStrictEqual([ + '555-0000', + undefined, + undefined, + '555-0003', + ]) + + fields[1]!.handleChange('555-0001') + + expect(fields.map((field) => field.value)).toStrictEqual([ + '555-0000', + '555-0001', + undefined, + '555-0003', + ]) + }) }) describe('InternalFormApi - reset', () => { @@ -1053,6 +1080,21 @@ describe('InternalFormApi - resetField', () => { expect(form.state.values).toEqual({ person: { name: 'Carol' } }) expect(form.state.isTouched).toBe(true) }) + it('restores the default at its own index when the array is shorter than the defaults', () => { + const form = new InternalFormApi({ + defaultValues: { phones: ['a', 'b', 'c', 'd'] }, + }) + + form.setFieldValue('phones', ['a']) + form.resetField('phones[3]') + + expect(form.state.values.phones).toStrictEqual([ + 'a', + undefined, + undefined, + 'd', + ]) + }) }) describe('InternalFormApi - deleteField', () => { diff --git a/packages/form-core/tests/utils.test.ts b/packages/form-core/tests/utils.test.ts index be555831bf..3451d66e14 100644 --- a/packages/form-core/tests/utils.test.ts +++ b/packages/form-core/tests/utils.test.ts @@ -99,7 +99,11 @@ describe('utils', () => { items: [undefined, undefined, { name: 'Ada' }], }) - expect(setBy(['first'], '[2]', 'third')).toEqual(['first', 'third']) + expect(setBy(['first'], '[2]', 'third')).toStrictEqual([ + 'first', + undefined, + 'third', + ]) expect(setBy([], '[2]', 'third')).toEqual([undefined, undefined, 'third']) expect(setBy(undefined, [undefined], 'third')).toEqual([undefined, 'third']) })