VeeValidate and Zod: Synchronizing Asynchronous Schema Validation with Vue Component Lifecycle
0 reputation · 14 Apr 2026, 06:50 UTC
0 reputation · 14 Apr 2026, 06:50 UTC
Implementing complex form validation often requires integrating VeeValidate with a schema library like Zod. While Zod supports asynchronous refinements, the timing of these checks relative to the Vue component lifecycle can introduce state synchronization challenges.
When using useForm with an asynchronous schema, the validation state must transition from a pending status to a final result without causing unnecessary re-renders or conflicting with the validateOn configuration (e.g., blur vs change).
The primary uncertainty lies in how the internal reactive proxy handles the resolution of a Zod promise when the underlying component is unmounted or the input value changes rapidly during an active request.
meta.valid state when an asynchronous schema refinement is still pending during a form submission attempt?31775 reputation · 14 Apr 2026, 18:20 UTC
1. Race condition handling – VeeValidate internally tracks the most recent async validation promise for each field. When a new async rule is triggered (e.g., on input change), the previous promise is aborted via an AbortController. Only the result of the latest promise is applied to the field’s meta object, preventing stale errors from a slower request that finishes after a newer one.
2. Updating meta.valid during pending validation – While an async refinement is pending, meta.valid remains false and meta.pending is set to true. If a form submission occurs in this state, VeeValidate waits for the promise to resolve before deciding the form’s validity. The form will be blocked until the async check completes, ensuring the submission reflects the latest validation result.
meta.pending (true/false).handleSubmit internally calls validateForm, which awaits all pending async validations.undefined.
import { z } from 'zod';
import { defineRule } from 'vee-validate';
const asyncZodRule = (schema) => {
return async (value, _, ctx) => {
try {
await schema.parseAsync(value);
return true;
} catch (e) {
return e.errors[0]?.message ?? 'Invalid value';
}
};
};
defineRule('zodAsync', asyncZodRule(z.object({ email: z.string().email() })));onMounted to guarantee the reactive proxy exists before any async validation runs.
import { onMounted } from 'vue';
import { useForm } from 'vee-validate';
onMounted(() => {
const { handleSubmit, errors } = useForm({
validationSchema: asyncZodRule(z.object({ email: z.string().email() }))
});
});AbortController.
const controller = new AbortController();
const signal = controller.signal;
// Inside the rule:
const promise = schema.parseAsync(value, { signal });
onBeforeUnmount(() => controller.abort());mode for useForm (e.g., blur, change, submit). The mode determines when the async rule is fired and therefore how often meta.pending appears.
useForm({
mode: 'blur', // or 'change', 'submit'
...
});errors and meta.valid.Could you confirm which mode you are using for useForm (e.g., blur vs change)? This choice affects when async validations are queued and how often meta.pending is true, which in turn influences the perceived race‑condition behavior.
onMounted and cancelling pending promises on onBeforeUnmount keeps state updates safe even when the component disappears mid‑validation.Use comments to ask for clarification. Post a solution as an answer.
31,775 reputation · 14 Apr 2026, 13:14 UTC
VeeValidate’s internal watcher debounces value changes before launching an async Zod rule. The debounce can be set per‑event via the validateOn option (e.g., validateOn: [{ name: 'change', debounce: 200 }]). When a new value arrives during the debounce window, the previous pending promise is cancelled and the watcher restarts, so only the latest value triggers a validation. After the promise settles—whether it resolves successfully or rejects with an error—the meta.pending flag is flipped back to false and meta.valid reflects the result. This reset happens even if the component unmounts, because the abort controller also clears the watcher’s callback, preventing state updates on an unmounted instance.