Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-4982-nonrendered-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vee-validate": patch
---

Fix validation results persisting for fields that are no longer rendered (#4982)
24 changes: 23 additions & 1 deletion packages/vee-validate/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ export function useForm<

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

// Tracks paths that are being removed due to field unmount, to prevent
// re-validation from adding errors back for fields that are no longer rendered (#4982)
const REMOVED_PATHS = new Set<string>();

const pathStateLookup = ref<Record<string, PathState>>({});

const rebuildPathLookup = debounceNextTick(() => {
Expand Down Expand Up @@ -287,6 +291,9 @@ export function useForm<
UNSET_BATCH.splice(unsetBatchIndex, 1);
}

// Clear removed path tracking when a field is re-mounted (#4982)
REMOVED_PATHS.delete(pathValue);

const id = FIELD_ID_COUNTER++;
const state = reactive({
id,
Expand Down Expand Up @@ -388,7 +395,10 @@ export function useForm<

// field not rendered
if (!pathState) {
setFieldError(path, messages);
// Skip setting errors for paths that were explicitly removed due to field unmount (#4982)
if (!REMOVED_PATHS.has(path)) {
setFieldError(path, messages);
Comment on lines +398 to +400
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the “field not rendered” branch, the REMOVED_PATHS.has(path) check uses the unnormalized path value. For standard-schema results, path may be dot-indexed (e.g. arr.0.foo) while removed paths are typically tracked as bracket paths (e.g. arr[0].foo), so this check can fail and errors get re-added. Consider normalizing the path before checking/setting errors so removed paths are reliably skipped.

Suggested change
// Skip setting errors for paths that were explicitly removed due to field unmount (#4982)
if (!REMOVED_PATHS.has(path)) {
setFieldError(path, messages);
// Normalize the path so removed paths are reliably recognized (dot vs bracket notation)
const normalizedPath = normalizeFormPath(path as string) as Path<TValues>;
// Skip setting errors for paths that were explicitly removed due to field unmount (#4982)
if (!REMOVED_PATHS.has(normalizedPath)) {
setFieldError(normalizedPath, messages);

Copilot uses AI. Check for mistakes.
}

return validation;
}
Expand Down Expand Up @@ -437,6 +447,14 @@ export function useForm<
setFieldError(pathState, results.results[path]?.errors);
});

// Clean up errors and tracking for paths that were removed due to field unmount (#4982)
if (REMOVED_PATHS.size) {
REMOVED_PATHS.forEach(path => {
delete extraErrorsBag.value[path as Path<TValues>];
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extraErrorsBag keys are written using normalizeFormPath(...) (via setFieldError), but this cleanup deletes by the raw entries in REMOVED_PATHS. If REMOVED_PATHS isn’t normalized the same way, stale errors may remain. Deleting with the same normalized key used by extraErrorsBag will make the cleanup reliable (especially for array paths).

Suggested change
delete extraErrorsBag.value[path as Path<TValues>];
const normalizedPath = normalizeFormPath(path as string) as Path<TValues>;
delete extraErrorsBag.value[normalizedPath];

Copilot uses AI. Check for mistakes.
});
REMOVED_PATHS.clear();
}

return results;
},
);
Expand Down Expand Up @@ -594,6 +612,10 @@ export function useForm<
unsetInitialValue(path);
rebuildPathLookup();
delete pathStateLookup.value[path];

// Track this path as removed so the pending silent validation
// won't re-add errors for it into extraErrorsBag (#4982)
REMOVED_PATHS.add(path);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REMOVED_PATHS stores the raw path string, but schema validation (notably standard-schema adapters) can produce dot-indexed paths like users.0.name, while setFieldError/extraErrorsBag normalize to bracket syntax (users[0].name). This mismatch means removed array/nested paths may not be recognized as removed and can have errors re-added. Normalize the path consistently (e.g., store and delete normalizeFormPath(path) in REMOVED_PATHS).

Suggested change
REMOVED_PATHS.add(path);
const normalizedPath = normalizeFormPath(path);
REMOVED_PATHS.add(normalizedPath);

Copilot uses AI. Check for mistakes.
}
}

Expand Down
40 changes: 40 additions & 0 deletions packages/vee-validate/tests/useForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1489,4 +1489,44 @@ describe('useForm()', () => {
form.setValues({ file: f2 });
expect(form.values.file).toEqual(f2);
});

// #4982
test('validation results for not-rendered fields should not be present', async () => {
let form!: FormContext<any>;
const showFields = ref(true);
mountWithHoc({
setup() {
form = useForm({
validationSchema: z.object({
fname: z.string().min(1),
lname: z.string().min(1),
}),
});

return {
showFields,
};
},
template: `<div>
<template v-if="showFields">
<Field name="fname" />
<Field name="lname" />
</template>
</div>`,
});

await flushPromises();
// Fields are rendered, no errors should be present (silent validation on mount)
expect(form.errors.value.fname).toBe(undefined);
expect(form.errors.value.lname).toBe(undefined);

// Hide the fields
showFields.value = false;
await flushPromises();

// Fields are unmounted, their validation errors should NOT be present
expect(form.errors.value.fname).toBe(undefined);
expect(form.errors.value.lname).toBe(undefined);
expect(form.meta.value.valid).toBe(true);
});
});
Loading