Repository navigation
deleteField also deletes sibling fields whose name merely shares a string prefix #2317
Description
Describe the bug
FormApi.deleteField(name) selects sub-fields to delete with a raw string startsWith check:
// packages/form-core/src/FormApi.ts
deleteField = <TField extends DeepKeys<TFormData>>(field: TField) => {
const subFieldsToDelete = Object.keys(this.fieldInfo).filter((f) => {
const fieldStr = field.toString()
return f !== fieldStr && f.startsWith(fieldStr)
})
const fieldsToDelete = [...subFieldsToDelete, field]
...
}startsWith has no path-boundary check, so any registered field whose name begins with the deleted name is treated as a sub-field. Deleting email therefore also deletes emailVerified — its value, its fieldInfo entry, and its fieldMetaBase entry — even though the two are unrelated siblings.
This looks like an unintended side effect of the fix for #1178 (Deleting field keeps data from subfields), which needed to catch genuine sub-fields (parent.child, parent[0]).
The failure is silent: the sibling's value simply disappears from form.state.values, and because its fieldInfo is gone too, a currently-mounted <Field> for it loses its registration. We hit this with a conditionally rendered field that calls deleteField when it unmounts — hiding it wiped an unrelated sibling whose name happened to start with the same string, and that sibling then rendered blank and failed its required-field validation.
Your minimal, reproducible example
Steps to reproduce
- Create a form with two sibling fields where one name is a string prefix of the other (
emailandemailVerified). - Register both fields (render a
<Field>for each, or callgetFieldInfoas above). - Call
form.deleteField('email'). - Read
form.getFieldValue('emailVerified')— it isundefined, and the key is gone fromform.state.values.
The same happens for array paths, e.g. deleting items[0].price wipes items[0].priceCurrency.
Expected behavior
deleteField('email') should delete only email and its genuine sub-fields (email.x, email[0]). Sibling fields that merely share a name prefix should be untouched: emailVerified should still hold true, and its fieldInfo / fieldMetaBase entries should survive.
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
- macOS 26.6
- Node 24.4.1 (also reproduced in Chrome via the React adapter)
TanStack Form adapter
react-form
TanStack Form version
1.33.4 (also present on main as of this writing, and in 1.28.0)
TypeScript version
5.9.3
Additional context
A path-boundary check looks sufficient, and preserves the behavior #1178 asked for:
deleteField = <TField extends DeepKeys<TFormData>>(field: TField) => {
const subFieldsToDelete = Object.keys(this.fieldInfo).filter((f) => {
const fieldStr = field.toString()
- return f !== fieldStr && f.startsWith(fieldStr)
+ return f.startsWith(`${fieldStr}.`) || f.startsWith(`${fieldStr}[`)
})Genuine sub-fields are always separated from their parent by . or [, so the two cases above cover them; f !== fieldStr becomes unnecessary because neither prefix can match the field itself.
I applied that patch to 1.33.4 locally and checked the three cases:
| case | result |
|---|---|
deleteField('email') |
emailVerified keeps its value ✅ |
deleteField('parent') with parent.child registered |
parent.child deleted from values and fieldInfo ✅ |
deleteField('rows[0]') with rows[0].a registered |
rows[0].a deleted from values and fieldInfo ✅ |
Worth noting that deleteBy (used for the values) is already path-aware — it is only the fieldInfo scan that does string matching, so the values of legitimately nested sub-fields are removed correctly either way.
Activity
fix(form-core): preserve prefix-matching sibling fields (#2318)