[v2][vue] `Field` keeps a stale field API after `ArrayField` `removeValue()` shifts items, so shifted errors aren't rendered
Nobody has claimed this yet.
- 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
- Render the component and trigger validation once (for example
form.setFieldValue('issues[0].title', 'A')). Row 1 shows "Required". - Click Remove on row 0.
- The remaining row is now
issues[0]andform.getFieldMeta('issues[0].title').errorsis["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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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