Skip to content

Commit 4a5ac89

Browse files
logaretmclaude
andcommitted
fix: clean up validation state when fields are unmounted (#4982)
When fields are conditionally hidden with v-if, their validation results (errors, meta) were persisting in the form state. This happened because removePathState triggered a silent re-validation that would store errors in extraErrorsBag for the now-removed fields. The fix tracks paths that are being removed due to field unmount and prevents the re-validation from adding errors back for those paths. When a field is re-mounted, the tracking is cleared so validation works normally again. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d8cc52 commit 4a5ac89

3 files changed

Lines changed: 68 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"vee-validate": patch
3+
---
4+
5+
Fix validation results persisting for fields that are no longer rendered (#4982)

packages/vee-validate/src/useForm.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ export function useForm<
129129

130130
const extraErrorsBag: Ref<FormErrorBag<TValues>> = ref({});
131131

132+
// Tracks paths that are being removed due to field unmount, to prevent
133+
// re-validation from adding errors back for fields that are no longer rendered (#4982)
134+
const REMOVED_PATHS = new Set<string>();
135+
132136
const pathStateLookup = ref<Record<string, PathState>>({});
133137

134138
const rebuildPathLookup = debounceNextTick(() => {
@@ -287,6 +291,9 @@ export function useForm<
287291
UNSET_BATCH.splice(unsetBatchIndex, 1);
288292
}
289293

294+
// Clear removed path tracking when a field is re-mounted (#4982)
295+
REMOVED_PATHS.delete(pathValue);
296+
290297
const id = FIELD_ID_COUNTER++;
291298
const state = reactive({
292299
id,
@@ -388,7 +395,10 @@ export function useForm<
388395

389396
// field not rendered
390397
if (!pathState) {
391-
setFieldError(path, messages);
398+
// Skip setting errors for paths that were explicitly removed due to field unmount (#4982)
399+
if (!REMOVED_PATHS.has(path)) {
400+
setFieldError(path, messages);
401+
}
392402

393403
return validation;
394404
}
@@ -437,6 +447,14 @@ export function useForm<
437447
setFieldError(pathState, results.results[path]?.errors);
438448
});
439449

450+
// Clean up errors and tracking for paths that were removed due to field unmount (#4982)
451+
if (REMOVED_PATHS.size) {
452+
REMOVED_PATHS.forEach(path => {
453+
delete extraErrorsBag.value[path as Path<TValues>];
454+
});
455+
REMOVED_PATHS.clear();
456+
}
457+
440458
return results;
441459
},
442460
);
@@ -594,6 +612,10 @@ export function useForm<
594612
unsetInitialValue(path);
595613
rebuildPathLookup();
596614
delete pathStateLookup.value[path];
615+
616+
// Track this path as removed so the pending silent validation
617+
// won't re-add errors for it into extraErrorsBag (#4982)
618+
REMOVED_PATHS.add(path);
597619
}
598620
}
599621

packages/vee-validate/tests/useForm.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,4 +1489,44 @@ describe('useForm()', () => {
14891489
form.setValues({ file: f2 });
14901490
expect(form.values.file).toEqual(f2);
14911491
});
1492+
1493+
// #4982
1494+
test('validation results for not-rendered fields should not be present', async () => {
1495+
let form!: FormContext<any>;
1496+
const showFields = ref(true);
1497+
mountWithHoc({
1498+
setup() {
1499+
form = useForm({
1500+
validationSchema: z.object({
1501+
fname: z.string().min(1),
1502+
lname: z.string().min(1),
1503+
}),
1504+
});
1505+
1506+
return {
1507+
showFields,
1508+
};
1509+
},
1510+
template: `<div>
1511+
<template v-if="showFields">
1512+
<Field name="fname" />
1513+
<Field name="lname" />
1514+
</template>
1515+
</div>`,
1516+
});
1517+
1518+
await flushPromises();
1519+
// Fields are rendered, no errors should be present (silent validation on mount)
1520+
expect(form.errors.value.fname).toBe(undefined);
1521+
expect(form.errors.value.lname).toBe(undefined);
1522+
1523+
// Hide the fields
1524+
showFields.value = false;
1525+
await flushPromises();
1526+
1527+
// Fields are unmounted, their validation errors should NOT be present
1528+
expect(form.errors.value.fname).toBe(undefined);
1529+
expect(form.errors.value.lname).toBe(undefined);
1530+
expect(form.meta.value.valid).toBe(true);
1531+
});
14921532
});

0 commit comments

Comments
 (0)