Repository: TanStack/form
Pull request #1241: fix(form-core): listen to array onChange events
Author: login: pavle99
Created: 2025-03-07T12:11:33Z | Merged: 2025-03-08T13:23:28Z

Listen to array field events

Closes #1226 

Linked issue #1226: Listener not run on array form field
### Describe the bug

An `onChange` listener on a `<form.Field mode="array">` is not run when there are changes to the input

### Your minimal, reproducible example

https://stackblitz.com/edit/vitejs-vite-n6la3gan?file=src%2FApp.tsx

### Steps to reproduce

Example Steps:
1. Add a person
2. Notice that the household size does not increase

General Steps:

1. add an `onChange` listener to a `<form.Field mode="array">`
2. use `field.pushValue` to update the field
3. Observe that the `onChange` is not called

### Expected behavior

Would expect the `onChange` listener to be called when a value is changed.

### How often does this bug happen?

Every time

### Screenshots or Videos

https://github.com/user-attachments/assets/d3655700-a7ed-4d8d-9b67-c7fc460d6c49

### Platform

- OS: MacOS
- Browser: Arc v1.82.1 
- Chromium Engine Version: 133.0.6943.35

### TanStack Form adapter

react-form

### TanStack Form version

v1.0.0

### TypeScript version

v5.7.2

### Additional context

_No response_
Linked issue comments: user: login: harry-whorlow
created_at: 2025-03-07T11:57:19Z
body: @tmcdo1 I'll take a look, I have a feeling I know why.... I apologise for this oversight

author_association: CONTRIBUTOR
user: login: tmcdo1
created_at: 2025-03-07T16:12:44Z
body: > I'll take a look, I have a feeling I know why.... I apologise for this oversight

No worries and thanks for all the hard work!
author_association: NONE
user: login: harry-whorlow
created_at: 2025-03-08T13:25:23Z
body: @tmcdo1 #1241 was just merged, this should fix the issue. Let me know if it persists 🤟 
author_association: CONTRIBUTOR

Diff:
diff --git a/docs/reference/classes/fieldapi.md b/docs/reference/classes/fieldapi.md
index be77a75c5..4b06bdeef 100644
--- a/docs/reference/classes/fieldapi.md
+++ b/docs/reference/classes/fieldapi.md
@@ -213,7 +213,7 @@ Use `field.state.value` instead.
 handleBlur(): void
 ```
 
-Defined in: [packages/form-core/src/FieldApi.ts:1593](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1593)
+Defined in: [packages/form-core/src/FieldApi.ts:1632](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1632)
 
 Handles the blur event.
 
@@ -229,7 +229,7 @@ Handles the blur event.
 handleChange(updater): void
 ```
 
-Defined in: [packages/form-core/src/FieldApi.ts:1586](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1586)
+Defined in: [packages/form-core/src/FieldApi.ts:1625](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1625)
 
 Handles the change event.
 
@@ -251,10 +251,10 @@ Handles the change event.
 insertValue(
    index, 
    value, 
-opts?): Promise<void>
+   opts?): void
 ```
 
-Defined in: [packages/form-core/src/FieldApi.ts:1234](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1234)
+Defined in: [packages/form-core/src/FieldApi.ts:1241](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1241)
 
 Inserts a value at the specified index, shifting the subsequent values to the right.
 
@@ -274,7 +274,7 @@ Inserts a value at the specified index, shifting the subsequent values to the ri
 
 #### Returns
 
-`Promise`\<`void`\>
+`void`
 
 ***
 
@@ -307,7 +307,7 @@ moveValue(
    opts?): void
 ```
 
-Defined in: [packages/form-core/src/FieldApi.ts:1264](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1264)
+Defined in: [packages/form-core/src/FieldApi.ts:1297](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1297)
 
 Moves the value at the first specified index to the second specified index.
 
@@ -360,10 +360,10 @@ Pushes a new value to the field.
 ### removeValue()
 
 ```ts
-removeValue(index, opts?): Promise<void>
+removeValue(index, opts?): void
 ```
 
-Defined in: [packages/form-core/src/FieldApi.ts:1252](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1252)
+Defined in: [packages/form-core/src/FieldApi.ts:1273](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1273)
 
 Removes a value at the specified index.
 
@@ -379,7 +379,7 @@ Removes a value at the specified index.
 
 #### Returns
 
-`Promise`\<`void`\>
+`void`
 
 ***
 
@@ -389,10 +389,10 @@ Removes a value at the specified index.
 replaceValue(
    index, 
    value, 
-opts?): Promise<void>
+   opts?): void
 ```
 
-Defined in: [packages/form-core/src/FieldApi.ts:1243](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1243)
+Defined in: [packages/form-core/src/FieldApi.ts:1257](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1257)
 
 Replaces a value at the specified index.
 
@@ -412,7 +412,7 @@ Replaces a value at the specified index.
 
 #### Returns
 
-`Promise`\<`void`\>
+`void`
 
 ***
 
@@ -422,7 +422,7 @@ Replaces a value at the specified index.
 setErrorMap(errorMap): void
 ```
 
-Defined in: [packages/form-core/src/FieldApi.ts:1613](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1613)
+Defined in: [packages/form-core/src/FieldApi.ts:1652](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1652)
 
 Updates the field's errorMap
 
@@ -495,7 +495,7 @@ swapValues(
    opts?): void
 ```
 
-Defined in: [packages/form-core/src/FieldApi.ts:1258](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1258)
+Defined in: [packages/form-core/src/FieldApi.ts:1285](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1285)
 
 Swaps the values at the specified indices.
 
@@ -547,7 +547,7 @@ Updates the field instance with new options.
 validate(cause, opts?): unknown[] | Promise<unknown[]>
 ```
 
-Defined in: [packages/form-core/src/FieldApi.ts:1553](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1553)
+Defined in: [packages/form-core/src/FieldApi.ts:1592](https://github.com/TanStack/form/blob/main/packages/form-core/src/FieldApi.ts#L1592)
 
 Validates the field value.
 
diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts
index 91468d56f..3013e7aaa 100644
--- a/packages/form-core/src/FieldApi.ts
+++ b/packages/form-core/src/FieldApi.ts
@@ -1226,7 +1226,14 @@ export class FieldApi<
   pushValue = (
     value: TData extends any[] ? TData[number] : never,
     opts?: UpdateMetaOptions,
-  ) => this.form.pushFieldValue(this.name, value as any, opts)
+  ) => {
+    this.form.pushFieldValue(this.name, value as any, opts)
+
+    this.options.listeners?.onChange?.({
+      value: this.state.value,
+      fieldApi: this,
+    })
+  }
 
   /**
    * Inserts a value at the specified index, shifting the subsequent values to the right.
@@ -1235,7 +1242,14 @@ export class FieldApi<
     index: number,
     value: TData extends any[] ? TData[number] : never,
     opts?: UpdateMetaOptions,
-  ) => this.form.insertFieldValue(this.name, index, value as any, opts)
+  ) => {
+    this.form.insertFieldValue(this.name, index, value as any, opts)
+
+    this.options.listeners?.onChange?.({
+      value: this.state.value,
+      fieldApi: this,
+    })
+  }
 
   /**
    * Replaces a value at the specified index.
@@ -1244,26 +1258,51 @@ export class FieldApi<
     index: number,
     value: TData extends any[] ? TData[number] : never,
     opts?: UpdateMetaOptions,
-  ) => this.form.replaceFieldValue(this.name, index, value as any, opts)
+  ) => {
+    this.form.replaceFieldValue(this.name, index, value as any, opts)
+
+    this.options.listeners?.onChange?.({
+      value: this.state.value,
+      fieldApi: this,
+    })
+  }
 
   /**
    * Removes a value at the specified index.
    */
-  removeValue = (index: number, opts?: UpdateMetaOptions) =>
+  removeValue = (index: number, opts?: UpdateMetaOptions) => {
     this.form.removeFieldValue(this.name, index, opts)
 
+    this.options.listeners?.onChange?.({
+      value: this.state.value,
+      fieldApi: this,
+    })
+  }
+
   /**
    * Swaps the values at the specified indices.
    */
-  swapValues = (aIndex: number, bIndex: number, opts?: UpdateMetaOptions) =>
+  swapValues = (aIndex: number, bIndex: number, opts?: UpdateMetaOptions) => {
     this.form.swapFieldValues(this.name, aIndex, bIndex, opts)
 
+    this.options.listeners?.onChange?.({
+      value: this.state.value,
+      fieldApi: this,
+    })
+  }
+
   /**
    * Moves the value at the first specified index to the second specified index.
    */
-  moveValue = (aIndex: number, bIndex: number, opts?: UpdateMetaOptions) =>
+  moveValue = (aIndex: number, bIndex: number, opts?: UpdateMetaOptions) => {
     this.form.moveFieldValues(this.name, aIndex, bIndex, opts)
 
+    this.options.listeners?.onChange?.({
+      value: this.state.value,
+      fieldApi: this,
+    })
+  }
+
   /**
    * @private
    */
diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts
index 7365852e2..8175718f4 100644
--- a/packages/form-core/tests/FieldApi.spec.ts
+++ b/packages/form-core/tests/FieldApi.spec.ts
@@ -1156,6 +1156,46 @@ describe('field api', () => {
     expect(form.getFieldValue('greet')).toStrictEqual('hello baz')
   })
 
+  it('should run the onChange listener when the field array is changed', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['one', 'two'],
+      },
+    })
+    form.mount()
+
+    let arr!: string[]
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          arr = value
+        },
+      },
+    })
+    field.mount()
+
+    field.removeValue(1)
+    expect(arr).toStrictEqual(['one'])
+
+    field.replaceValue(0, 'start')
+    expect(arr).toStrictEqual(['start'])
+
+    field.pushValue('end')
+    expect(arr).toStrictEqual(['start', 'end'])
+
+    field.insertValue(1, 'middle')
+    expect(arr).toStrictEqual(['start', 'middle', 'end'])
+
+    field.swapValues(0, 2)
+    expect(arr).toStrictEqual(['end', 'middle', 'start'])
+
+    field.moveValue(0, 1)
+    expect(arr).toStrictEqual(['middle', 'end', 'start'])
+  })
+
   it('should reset the form on a listener', () => {
     const form = new FormApi({
       defaultValues: {


Reviews:
user: login: harry-whorlow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T14:22:43Z
user: login: pavle99
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T14:54:24Z
user: login: harry-whorlow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T15:15:30Z
user: login: pavle99
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T15:20:33Z
user: login: harry-whorlow
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T15:23:48Z
user: login: pavle99
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T15:25:54Z

Review comments:
diff_hunk: @@ -1244,26 +1262,59 @@ export class FieldApi<
     index: number,
     value: TData extends any[] ? TData[number] : never,
     opts?: UpdateMetaOptions,
-  ) => this.form.replaceFieldValue(this.name, index, value as any, opts)
+  ) => {
+    this.form.replaceFieldValue(this.name, index, value as any, opts)
+
+    this.options.listeners?.onChange?.({
+      value: this.state.value,
+      fieldApi: this,
+    })
+
+    this.validate('change')
+  }
 
   /**
    * Removes a value at the specified index.
    */
-  removeValue = (index: number, opts?: UpdateMetaOptions) =>
+  removeValue = (index: number, opts?: UpdateMetaOptions) => {
     this.form.removeFieldValue(this.name, index, opts)
 
+    this.options.listeners?.onChange?.({
+      value: this.state.value,
+      fieldApi: this,
+    })
+
+    this.validate('change')
path: packages/form-core/src/FieldApi.ts
user: login: harry-whorlow
body: what was the reasoning for adding this? I also think it's called in form when you call     `this.form.removeFieldValue(this.name, index, opts)` 
as shown by this line: `await this.validateField(field, 'change')` in the formApi


created_at: 2025-03-07T14:22:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1244,26 +1262,59 @@ export class FieldApi<
     index: number,
     value: TData extends any[] ? TData[number] : never,
     opts?: UpdateMetaOptions,
-  ) => this.form.replaceFieldValue(this.name, index, value as any, opts)
+  ) => {
+    this.form.replaceFieldValue(this.name, index, value as any, opts)
+
+    this.options.listeners?.onChange?.({
+      value: this.state.value,
+      fieldApi: this,
+    })
+
+    this.validate('change')
+  }
 
   /**
    * Removes a value at the specified index.
    */
-  removeValue = (index: number, opts?: UpdateMetaOptions) =>
+  removeValue = (index: number, opts?: UpdateMetaOptions) => {
     this.form.removeFieldValue(this.name, index, opts)
 
+    this.options.listeners?.onChange?.({
+      value: this.state.value,
+      fieldApi: this,
+    })
+
+    this.validate('change')
path: packages/form-core/src/FieldApi.ts
user: login: pavle99
body: You are right, will remove the uneccessary validations.
created_at: 2025-03-07T14:54:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1156,6 +1156,196 @@ describe('field api', () => {
     expect(form.getFieldValue('greet')).toStrictEqual('hello baz')
   })
 
+  it('should change the form state when running listener onChange with pushValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsCount: 2,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.pushValue('third item')
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(3)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'second item',
+      'third item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with insertValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsCount: 2,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.insertValue(1, 'middle item')
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(3)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'middle item',
+      'second item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with replaceValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.replaceValue(0, 'replaced item')
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'replaced item',
+      'second item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with removeValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsCount: 3,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.removeValue(1)
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(2)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'third item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with swapValues', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.swapValues(0, 2)
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'third item',
+      'second item',
+      'first item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with moveValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.moveValue(0, 2)
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'second item',
+      'third item',
+      'first item',
+    ])
+  })
+
path: packages/form-core/tests/FieldApi.spec.ts
user: login: harry-whorlow
body: I'm not sure these need to be all separate tests, they could all just be one test where at each step the utils are tested then be grouped under 'should listen to array field changes'. 

Saves re-instantiating form each and every time, and saves some line count, whilst also testing the same stuff. 

What do you think?
created_at: 2025-03-07T15:15:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1156,6 +1156,196 @@ describe('field api', () => {
     expect(form.getFieldValue('greet')).toStrictEqual('hello baz')
   })
 
+  it('should change the form state when running listener onChange with pushValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsCount: 2,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.pushValue('third item')
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(3)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'second item',
+      'third item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with insertValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsCount: 2,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.insertValue(1, 'middle item')
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(3)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'middle item',
+      'second item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with replaceValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.replaceValue(0, 'replaced item')
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'replaced item',
+      'second item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with removeValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsCount: 3,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.removeValue(1)
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(2)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'third item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with swapValues', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.swapValues(0, 2)
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'third item',
+      'second item',
+      'first item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with moveValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.moveValue(0, 2)
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'second item',
+      'third item',
+      'first item',
+    ])
+  })
+
path: packages/form-core/tests/FieldApi.spec.ts
user: login: pavle99
body: Sure, we can do it like that as well. I separated them so it's clearer to know what test failed specifically if a functionality of some method fails in the future
created_at: 2025-03-07T15:20:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1156,6 +1156,196 @@ describe('field api', () => {
     expect(form.getFieldValue('greet')).toStrictEqual('hello baz')
   })
 
+  it('should change the form state when running listener onChange with pushValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsCount: 2,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.pushValue('third item')
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(3)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'second item',
+      'third item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with insertValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsCount: 2,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.insertValue(1, 'middle item')
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(3)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'middle item',
+      'second item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with replaceValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.replaceValue(0, 'replaced item')
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'replaced item',
+      'second item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with removeValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsCount: 3,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.removeValue(1)
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(2)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'third item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with swapValues', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.swapValues(0, 2)
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'third item',
+      'second item',
+      'first item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with moveValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.moveValue(0, 2)
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'second item',
+      'third item',
+      'first item',
+    ])
+  })
+
path: packages/form-core/tests/FieldApi.spec.ts
user: login: harry-whorlow
body: Yeah, I see your point. To be honest I don't really have a strong opinion on it.... Looking back on my tests I did the same. So, we can leave it as it. 🤟

created_at: 2025-03-07T15:23:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1156,6 +1156,196 @@ describe('field api', () => {
     expect(form.getFieldValue('greet')).toStrictEqual('hello baz')
   })
 
+  it('should change the form state when running listener onChange with pushValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsCount: 2,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.pushValue('third item')
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(3)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'second item',
+      'third item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with insertValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsCount: 2,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.insertValue(1, 'middle item')
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(3)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'middle item',
+      'second item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with replaceValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.replaceValue(0, 'replaced item')
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'replaced item',
+      'second item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with removeValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsCount: 3,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: ({ value }) => {
+          form.setFieldValue('itemsCount', value.length)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.removeValue(1)
+
+    expect(form.getFieldValue('itemsCount')).toStrictEqual(2)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'first item',
+      'third item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with swapValues', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.swapValues(0, 2)
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'third item',
+      'second item',
+      'first item',
+    ])
+  })
+
+  it('should change the form state when running listener onChange with moveValue', () => {
+    const form = new FormApi({
+      defaultValues: {
+        items: ['first item', 'second item', 'third item'],
+        itemsModified: false,
+      },
+    })
+
+    form.mount()
+
+    const field = new FieldApi({
+      form,
+      name: 'items',
+      listeners: {
+        onChange: () => {
+          form.setFieldValue('itemsModified', true)
+        },
+      },
+    })
+
+    field.mount()
+
+    field.moveValue(0, 2)
+
+    expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
+    expect(form.getFieldValue('items')).toStrictEqual([
+      'second item',
+      'third item',
+      'first item',
+    ])
+  })
+
path: packages/form-core/tests/FieldApi.spec.ts
user: login: pavle99
body: I can extract the tests into a separate function, since they repeat themselves as well and are in this form:
```ts
expect(form.getFieldValue('itemsCount')).toStrictEqual(<number>)
expect(form.getFieldValue('items')).toStrictEqual(<string_array>)
expect(form.getFieldValue('itemsModified')).toStrictEqual(true)
```

For eg:
```ts
type CheckFieldValuesArgs = {
  itemsCount: number
  items: string[]
}

const checkFieldValuesAndResetForNextTest = ({
  itemsCount,
  items,
}: CheckFieldValuesArgs) => {
  expect(form.getFieldValue('itemsCount')).toStrictEqual(itemsCount)
  expect(form.getFieldValue('items')).toStrictEqual(items)
  expect(form.getFieldValue('itemsModified')).toStrictEqual(true)

  form.setFieldValue('itemsModified', false)
}
```

WDYT?
created_at: 2025-03-07T15:25:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: nx-cloud[bot]
created_at: 2025-03-07T12:11:59Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/67cc2ade3d5d69306ae1910e?utm_source=pull-request&utm_medium=comment) for commit 56a0918eef3cc2a81339f31804339d33a1685d30.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx affected --targets=test:sherif,test:knip,tes...` | ✅ Succeeded | 2m 8s | [View ↗](https://cloud.nx.app/runs/Z2fdboo0s0?utm_source=pull-request&utm_medium=comment) |
| `nx run-many --target=build --exclude=examples/**` | ✅ Succeeded | 24s | [View ↗](https://cloud.nx.app/runs/lQbpd4G1bk?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-03-08 11:35:50` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE
user: login: pkg-pr-new[bot]
created_at: 2025-03-07T12:13:57Z
body: 
[Open in Stackblitz](https://pkg.pr.new/template/24fc6f89-dd01-4dd5-88a8-06be87702b1d)
<details><summary><b>More templates</b></summary><p>

- [@tanstack/form-example-angular-array](https://pkg.pr.new/template/6d2f27c5-fa38-4225-b2af-4c88caa8a7a7)
- [@tanstack/form-example-angular-simple](https://pkg.pr.new/template/ae03d775-c688-4c16-b43c-04903d79628c)
- [@tanstack/form-example-lit-simple](https://pkg.pr.new/template/2442e2ec-ec1a-4cb6-81d3-2f9ca2dc9365)
- [@tanstack/form-example-lit-ui-libraries](https://pkg.pr.new/template/5af41630-400c-4ba2-a173-5c5328cc38b5)
- [@tanstack/form-example-react-array](https://pkg.pr.new/template/f3299dd3-f0be-45a1-8d7f-5e3df3c2a7b8)
- [@tanstack/form-example-react-compiler](https://pkg.pr.new/template/29414d6d-8717-4845-988c-7c479412aa79)
- [@tanstack/field-errors-from-form-validators](https://pkg.pr.new/template/4b416de4-9410-4835-bb69-3bd2ca2f0cc8)
- [@tanstack/form-example-react-large-form](https://pkg.pr.new/template/536e5706-f0de-4456-ae8f-65ed6e4bef66)
- [@tanstack/form-example-react-next-server-actions](https://pkg.pr.new/template/5b25721d-1203-42c6-aa98-c9dc821ec6e3)
- [@tanstack/form-example-react-query-integration](https://pkg.pr.new/template/be8f340d-41f9-4821-9c8b-5731eefd1e9e)
- [@tanstack/form-example-remix](https://pkg.pr.new/template/0e964a16-ac8f-4658-be57-0ccaace9e455)
- [@tanstack/form-example-react-simple](https://pkg.pr.new/template/6e4cd2a2-6c64-417d-9854-7b055c19c2a1)
- [@tanstack/form-example-react-standard-schema](https://pkg.pr.new/template/6363d9d8-efca-46e9-a4b0-413208dfc404)
- [@tanstack/form-example-react-tanstack-start](https://pkg.pr.new/template/8f9fbdcb-47c6-48e5-b7b1-46819b3025c0)
- [@tanstack/form-example-react-ui-libraries](https://pkg.pr.new/template/2df62717-4066-4025-9996-bb9c701ca55f)
- [@tanstack/form-example-solid-array](https://pkg.pr.new/template/29c97c82-7b2d-4a18-a631-ef2cb37b4ff8)
- [@tanstack/form-example-solid-simple](https://pkg.pr.new/template/b296bf1f-c04d-46e1-a53b-cf6a14bbf781)
- [@tanstack/form-example-vue-array](https://pkg.pr.new/template/3466c4b3-3fd9-4ee4-95ba-0623be15b6e3)
- [@tanstack/form-example-vue-simple](https://pkg.pr.new/template/3f2e1a8c-c5e5-47e5-8849-a6679d73690e)

</p></details>
      
    


<details><summary><b>@tanstack/angular-form</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/angular-form@1241
```

</p></details>
      
    

<details><summary><b>@tanstack/form-core</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/form-core@1241
```

</p></details>
      
    

<details><summary><b>@tanstack/lit-form</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/lit-form@1241
```

</p></details>
      
    

<details><summary><b>@tanstack/react-form</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-form@1241
```

</p></details>
      
    

<details><summary><b>@tanstack/solid-form</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/solid-form@1241
```

</p></details>
      
    

<details><summary><b>@tanstack/vue-form</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/vue-form@1241
```

</p></details>
      
    

_commit: <a href="https://github.com/TanStack/form/runs/38422199087"><code>56a0918</code></a>_

author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-07T12:16:35Z
body: ## [Codecov](https://app.codecov.io/gh/TanStack/form/pull/1241?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TanStack) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.81%. Comparing base [(`062f55e`)](https://app.codecov.io/gh/TanStack/form/commit/062f55e9dd8fbc13eef535dcecdf40691928e486?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TanStack) to head [(`56a0918`)](https://app.codecov.io/gh/TanStack/form/commit/56a0918eef3cc2a81339f31804339d33a1685d30?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TanStack).
> Report is 10 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1241      +/-   ##
==========================================
+ Coverage   88.73%   88.81%   +0.07%     
==========================================
  Files          28       28              
  Lines        1261     1269       +8     
  Branches      331      330       -1     
==========================================
+ Hits         1119     1127       +8     
  Misses        127      127              
  Partials       15       15              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/TanStack/form/pull/1241?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TanStack).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TanStack).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: harry-whorlow
created_at: 2025-03-08T11:43:49Z
body: @pavle99 looks good! Thanks for the contribution... and fixing my mistake 🙃. Just hang tight for a bit and I'll merge it once I get a chance to double check it all 🤟
author_association: CONTRIBUTOR
user: login: harry-whorlow
created_at: 2025-03-08T13:23:57Z
body:  @pavle99 Thanks for the contribution! 
author_association: CONTRIBUTOR