Using VeeValidate Async Rules for Remote Username Checks in Vue 3
Learn how to defer form submission while checking username availability with VeeValidate's async validation, debounce rapid typing, and keep the UI responsive.
14 Oct 2025, 05:21 UTC

The problem: blocking submission while waiting for a server check
When a user types a username, many applications immediately call an endpoint to see if the name is free. If the call is made synchronously or without deferring form submission, the UI can feel sluggish: the submit button stays disabled until the request finishes, and rapid typing can trigger several overlapping requests that overwrite each other.
Thesis: use VeeValidate’s async validation rule to keep the form reactive
VeeValidate treats a rule that returns a promise as asynchronous. The framework waits for the promise to settle before marking the field valid or invalid, allowing you to show a loading state, disable the submit button, and still react to each keystroke without blocking the UI thread.
Defining the async rule
Create a rule that calls your availability API and resolves to true when the name is free. If the name is taken, throw a ValidationError (or return a falsy value) so VeeValidate shows the message.
import { extend } from 'vee-validate';
import axios from 'axios';
extend('usernameAvailable', {
// value is the current input string
validate: value => {
// Return a promise; VeeValidate will wait for it
return axios.get(`/api/users/check`, { params: { username: value } })
.then(response => {
// Assume the API returns { available: true } or false
if (response.data.available) {
return true;
}
// Throwing a ValidationError triggers the message below
return false;
})
.catch(() => {
// Network error – treat as unavailable to avoid false positives
return false;
});
},
message: 'Username is already taken',
});
Registering and using the rule
Place the extension in a plugin file (e.g., src/plugins/veevalidate.js) so it runs once when the application starts. Then bind the rule to an input with the v-validate directive.
<template>
<form @submit.prevent='onSubmit'>
<label>Username</label>
<input
v-model='username'
v-validate='usernameAvailable'
data-vv-name='username'
:disabled='submitting'
/>
<span v-if='errors.username'>{{ errors.username[0] }}</span>
<button type='submit' :disabled='submitting || !valid'>Sign up</button>
</form>
</template>
<script>
import { ref, reactive } from 'vue';
import { useForm } from 'vee-validate';
export default {
setup() {
const username = ref('');
const { handleSubmit, errors, valid } = useForm();
const submitting = ref(false);
const onSubmit = handleSubmit(values => {
submitting.value = true;
// call your real signup API here
// ...
submitting.value = false;
});
return { username, errors, valid, submitting, onSubmit };
}
};
</script>
Debounce and cancel overlapping requests
Typing fast can fire many requests; older responses might arrive after a newer one, causing the validation state to flip incorrectly. Wrap the API call in a debounce function (e.g., 300 ms) and cancel the previous Axios request using a cancel token or the AbortController.
let cancelTokenSrc = axios.CancelToken.source();
extend('usernameAvailable', {
validate: value => {
// Cancel any pending request
if (cancelTokenSrc) cancelTokenSrc.cancel();
cancelTokenSrc = axios.CancelToken.source();
return axios.get(`/api/users/check`, {
params: { username: value },
cancelToken: cancelTokenSrc.token
}).then(res => res.data.available)
.catch(err => {
if (axios.isCancel(err)) return false; // ignore cancelled
return false;
});
},
message: 'Username is already taken',
});
Worked example: seeing the validation flow
Open the component in a browser, open Vue Devtools, and watch the errors.username array. As you type 'alice', the rule sends a request; the field shows a loading state (you can bind a class to pending if you expose it). When the server replies with { available: true }, the error disappears and the submit button becomes enabled. If you quickly change to 'alice123', the previous request is cancelled, and only the latest request determines the outcome.
Trade‑offs and limitations
- Each keystroke (after debounce) generates an HTTP request, increasing server load. Ensure the endpoint is lightweight and idempotent.
- Network latency can still cause a perceptible delay; consider optimistic UI (show 'checking...') while waiting.
- If the API requires authentication, make sure the token is available when the rule runs.
Actionable closing
Start by adding the async rule shown above, plug it into your registration form, and verify the behavior with Vue Devtools and a throttled network (e.g., 3G in Chrome DevTools). Adjust the debounce time to balance responsiveness and request volume, and always cancel outdated calls to avoid race conditions. With these steps you keep the form responsive, give users instant feedback, and avoid blocking submission while the server validates the username.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.