Skip to content

[v2][vue] Field keeps a stale field API after ArrayField removeValue() shifts items, so shifted errors aren't rendered #2391

Open

Description

@awdr74100

Describe the bug

With index keys (as in examples/vue/array), after array.removeValue(0) the form creates a new field API instance for issues[0].title, but the mounted <form.Field :name="issues[0].title"> keeps the old instance. The form meta for issues[0].title contains the error that shifted from the removed row's neighbour, but the slot never renders it.

This happens with markup rendered directly inside the <form.Field> slot. It's unrelated to createFormHook.

Diagnosis (uses the internal _tryGetFieldApi only to compare instances):

Result
Before removal: slot field === form._tryGetFieldApi('issues[0].title') true
After removal: internal instance replaced true
After removal: slot field === internal instance false
After removal: form.getFieldMeta('issues[0].title').errors ["Required"]
After removal: rendered errors none

useField (src/VueForm/useField.lib.ts) only recreates the field API when form, name or resetVersion changes. After removeValue, the name stays the same, so the component isn't moved to the new instance.

Your minimal, reproducible example

<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'

const form = useForm({
  defaultValues: { issues: [{ title: 'A' }, { title: '' }] },
  errorVisibility: () => true,
  validators: [
    {
      triggers: ['change'],
      run: ({ value, createErrorMap }) => {
        const errors = createErrorMap()
        value.issues.forEach((issue, i) => {
          if (!issue.title) (errors.fields as Record<string, string>)[`issues[${i}].title`] = 'Required'
        })
        return errors
      },
    },
  ],
})
</script>

<template>
  <form.ArrayField name="issues" v-slot="{ field: array }">
    <div v-for="(_, i) in array.value" :key="i">
      <form.Field :name="`issues[${i}].title`" v-slot="{ field }">
        <input :value="field.value" @input="field.handleChange(($event.target as HTMLInputElement).value)" />
        <p v-for="error in field.errors" :key="error.message">{{ error.message }}</p>
      </form.Field>
      <button type="button" @click="array.removeValue(i)">Remove</button>
    </div>
  </form.ArrayField>
</template>

Steps to reproduce

  1. Render the component and trigger validation once (for example form.setFieldValue('issues[0].title', 'A')). Row 1 shows "Required".
  2. Click Remove on row 0.
  3. The remaining row is now issues[0] and form.getFieldMeta('issues[0].title').errors is ["Required"], but no error is rendered.

Expected behavior

After removing an item, the mounted Field for each index follows the field API the form now uses for that name, and renders its current value and errors.

How often does this bug happen?

Every time

Platform

  • macOS, Node 24
  • Reproduced in Vitest with happy-dom
  • Vue 3.6.0-rc.8

TanStack Form adapter

vue-form

TanStack Form version

2.0.0-alpha.2, plus the alpha branch at 6e5b809 with the fix from PR #2392 applied. This issue is separate from that PR.

TypeScript version

6.0.3

Additional context

I haven't included a fix, because it touches how useField identifies its field API and I'd rather hear which direction you prefer. Happy to open a PR.

Activity

  1. awdr74100 commented on Sep 16, 2026

    @awdr74100
    Author

    Following up with a fix: #2393.

    Two things I found while working on it.

    It is not Vue-specific. The React adapter resolves names the same way, so the same removal case fails there on alpha. The PR adds a regression test to packages/react-form/tests/FormField.spec.tsx that fails without the change.

    It is not only removeValue. swapValues and moveValue call _moveTo as well, so a mounted Field can end up holding a field API that is now reachable by a different name, without anything being killed.

    On the direction you asked about: rather than changing how useField identifies its field API, the PR adds a fieldTreeVersion atom to the form, bumped when a field API is killed or moved to a new name, and has the adapters include it when they resolve a name. Happy to take it another way if you prefer.

  2. LeCarbonator commented on Sep 19, 2026

    @LeCarbonator
    Contributor

    Is this not a problem with how you keyed data? The index key implies you're using stable data, then you're moving an element and noticing that it's stale.

  3. added
    v2This issue affects v2 as well.
    scope: vueThis issue specifically affects the vue adapter.
    area: runtimeThis issue affects the runtime of the library.
    type: undecidedThis issue is not yet confirmed a bug or accepted as enhancement.
    on Sep 19, 2026
  4. awdr74100 commented on Oct 31, 2026

    @awdr74100
    Author

    Thanks for taking a look, and sorry for the slow follow-up. You're right that for object arrays, keying by a stable id is the more robust pattern. I checked that it works on alpha (2b4fc5c): with key={item.id}, removeValue, swapValues, moveValue and insertValue keep values, errors and input targeting correct in both React and Vue. (In Vue, child components that read field.name also need #2460.)

    While retesting, I found that my original report understated what happens with index keys, and I missed some context that I think matters for whether index keys should be supported. I'd appreciate your view on the direction before I change anything.

    What happens with index keys on current alpha

    This setup mirrors examples/vue/array and examples/react/array: a string array ['A', '', 'C'], items keyed by index, fields rendered directly in the slot / render prop, and a form validator that marks empty items as Required. After each mutation I check every row, then type X into row 0:

    Mutation What row 0 shows Typing X into row 0
    removeValue(0) '' with no error; row 1 shows '' instead of C, so C is no longer visible Nothing is written: values stay ['', 'C']
    swapValues(0, 1) Still A, with field.name = items[1], so the list looks unswapped Writes to items[1]
    moveValue(0, 2) A, with field.name = items[2] Writes to items[2]
    insertValue(0, 'Z') A, so Z is not visible and C is rendered twice Writes to items[1]

    React (normal and StrictMode) and Vue give identical results. So it isn't only a missing error message, as I first wrote. The rendered row and the field it edits can belong to different items.

    Why I think index keys may still need to work

    As I understand it, the core intentionally keeps nested field APIs attached to their items when the array changes. The tests in array-methods.lib.spec.ts say so, and that's what makes stable keys work. So I don't think the core is doing anything wrong. The question is how a mounted component should map to a field API when its key is the index. I may be missing the intended design here, so please correct me:

    1. Primitive arrays have no stable key. The examples/*/array examples use string[]. Keying by value breaks on duplicates, and since the key changes on every keystroke, the input remounts and loses focus. As far as I can tell, ArrayField doesn't expose a per-item key, so the index is the only option there. (Libraries that require stable keys usually generate one per item; for example, vee-validate's useFieldArray exposes fields[].key.)

    2. Most of the v2 docs and examples use index keys. docs/arrays.md recommends person.id under "Keeping item identity for objects", but these use the index:

      • docs/migrate-from-v1.md: the v2 snippet (<form.Field key={index} name={people[${index}].name}>)
      • docs/framework/react/reference/interfaces/FieldGroupArrayFieldComponent.md: key={index}
      • examples/vue/array (:key="index"), examples/react/array and examples/preact/array (no key), examples/angular/array (track $index)

      The examples only push items, so they work as shipped. The problem starts once items are removed or reordered. Right after its snippet, the migration guide points to the same array methods for inserting, removing, moving and swapping.

    3. v1 handled this. The same four mutations with index keys behave correctly on @tanstack/vue-form@1.33.5: rows show the right values and errors, and typing into row 0 writes to items[0]. So an app that follows the migration guide and removes or reorders items keeps its index keys and starts editing the wrong items after upgrading, without any warning.

    Possible directions

    • A. Support index keys again. Mounted fields re-resolve their name when array mutations move or kill field APIs. That's what [v2] Fix: Move mounted fields onto the current field API after array mutations #2393 does, but it was written against an older alpha and needs rework after [v2] Vue reactivity now works #2401's useField changes. I'd also want to check that stable-keyed fields don't briefly bind to the wrong field API before their new name arrives, which would fire extra mount/unmount listeners.
    • B. Require stable keys. Expose a per-item key from ArrayField so primitive arrays have one, update the examples and the migration guide, and possibly warn in dev when a field's API moves while its component stays mounted.

    I'm happy to work on either, or on something else you'd prefer. If B is the direction, I'll close #2393 and help with the docs and examples instead. I can also update this issue's title and description so they reflect the broader behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs reproductionarea: runtimeThis issue affects the runtime of the library.scope: vueThis issue specifically affects the vue adapter.type: undecidedThis issue is not yet confirmed a bug or accepted as enhancement.v2This issue affects v2 as well.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions