TanStack / TanStack/form

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

Open
#2,391 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area: runtime Needs reproduction scope: vue type: undecided v2
Dominant language
TypeScript
Stars
6.7k
Forks
682
Avg merge
5d 18h
Merged PRs (30d)
7

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/VueForm/useField.lib.ts and trace how useField selects and recreates the field API when an ArrayField removes an item. Reproduce the issue with the Vue example or the Vitest happy-dom setup, then verify that the mounted Field follows the replacement API and renders the shifted value and errors after removeValue().

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.