Async validation race condition with validateOnMount in vee-validate v4
0 reputation · 29 Oct 2020, 00:03 UTC
0 reputation · 29 Oct 2020, 00:03 UTC
When using validateOnMount with asynchronous validation rules in vee-validate v4, rapid prop or schema changes can trigger overlapping validation requests. The library does not provide a built-in cancellation mechanism, so a slower earlier request may resolve after a faster later one, overwriting the current error state with stale results.
useField composable registers fields in a global FormContext map keyed by name; dynamic name changes without a key change leave duplicate registrations.Goal: determine a reliable pattern to ensure only the most recent validation cycle updates the field's error state without relying on external abort controllers for every rule.
validateOnMount behavior be scoped to a specific schema version so that stale cycles are ignored automatically?useField options (validateOnMount, keepValuesOnUnmount, manual validate calls) yields deterministic async validation ordering?33275 reputation · 29 Oct 2020, 01:30 UTC
vee-validate v4 does not expose a documented, public validation-cycle identifier you can check inside an async rule, and validateOnMount cannot be scoped to a schema version. The reliable pattern is to implement the cycle guard yourself: keep a monotonically increasing counter next to the field, capture it when the rule starts, and only commit the result if the counter hasn't moved. Combined with manual validate() calls (or a keyed remount when the schema changes), this guarantees last-write-wins ordering without per-rule AbortControllers.
Confirmed behavior in v4: useField registers into the form context keyed by name, async rules are plain promises with no cancellation hook, and nothing in the documented public API (useField, useForm, Field props) exposes a request id or abort signal. The question's premise — that a slow earlier request can overwrite a later result — is correct for any promise-based rule.
Assumption to flag: some community snippets reference an internal counter on the form context. Treat anything not in the official v4 API reference as an implementation detail that can change between minor releases; do not build on it. The userland counter below has no such dependency.
import { ref } from 'vue';
import { useField } from 'vee-validate';
const seq = ref(0);
const { value, errorMessage, validate } = useField('email', async (v) => {
const mySeq = ++seq.value; // claim this cycle
const ok = await checkEmailOnServer(v);
if (mySeq !== seq.value) {
// A newer validation started while we were in flight.
// Return success so we never write a stale error; the newer
// cycle owns the error state.
return true;
}
return ok || 'Email already taken';
});Because vee-validate awaits each rule and applies the result to the field's error state, discarding the stale cycle by returning true is enough — the newer invocation will set the definitive error when it resolves. Note the subtlety: this works because each trigger runs the rule again; the guard only prevents the old run from producing an error message.
If you want strict ordering rather than discard-on-resolve, drop validateOnMount and drive validation yourself:
const { validate, resetField } = useField('email', rule, {
validateOnMount: false,
keepValuesOnUnmount: false,
});
watch(() => props.schemaVersion, async () => {
resetField(); // clear stale errors from the old schema
await validate(); // awaited: no overlap with the next trigger
});Awaiting the returned promise serializes cycles, so a later trigger cannot be overtaken by an earlier one. This is the most deterministic composition of the options you listed.
validateOnMount fires once per field registration; it has no notion of schema versions. When the schema or field name changes, change the key on the <Field> component (or the component hosting useField) so Vue unmounts and re-registers the field. This also fixes the duplicate-registration problem you noted for dynamic names. The trade-off: a keyed remount drops transient DOM state such as focus.
await new Promise(r => setTimeout(r, Math.random() * 300)), and toggle the validating prop every 100 ms.await validate() resolves before the next starts).One missing detail that would change the recommendation: are your rules defined per-field via useField, or through a form-level Yup/Zod schema? Schema-level validation validates the whole form per cycle, so the guard belongs on the form's submit/meta state rather than inside individual rules.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.