Hook — 06 : 02 a.m., Barranquilla ↔ São Paulo code review
Caribbean dawn filtered through my Airbnb shutters when Gabriel, pair-programming from São Paulo, pinged: “Our signup form lets people enter ‘abcd’ as an email—investors aren’t amused.” He’d wired vanilla watchers to every input; maintenance was déjà vu hell. We hot-swapped the page to VeeValidate in minutes, replaced 120 lines of ad-hoc checks with a schema, and shipped fully localized messages before coffee cooled. That remote rescue frames today’s exploration of form-validation tactics in Vue 3—when a library shines, when a hand-rolled solution suffices, and how to keep bundles slim for prepaid data plans across Latin America.
Why Robust Validation Still Matters
- Data integrity — garbage in, bugs out.
- Accessibility — screen readers need clear, timely error cues.
- Performance — blocking server round-trips saves precious 3 G bytes.
- Developer speed — predictable patterns beat bespoke if-statements in every component.
Vue.js offers low-level reactivity for custom rules, while VeeValidate wraps years of UX research into declarative APIs. Picking the right approach per project saves headaches later.
Option 1: VeeValidate Quick-Start
Installation
bashCopyEditnpm i vee-validate@4 yup
yup
is optional but handy for schema validation.
Skeleton Form
vueCopyEdit<script setup lang="ts">
import { useForm, useField } from 'vee-validate';
import { object, string, ref as yref } from 'yup';
const schema = object({
email: string().email().required(),
pass: string().min(8).required(),
confirm: string().oneOf([yref('pass')], 'Passwords must match'),
});
const { handleSubmit } = useForm({ validationSchema: schema });
const { value: email, errorMessage: emailErr } = useField<string>('email');
const { value: pass, errorMessage: passErr } = useField<string>('pass');
const { value: confirm, errorMessage: confErr } = useField<string>('confirm');
const submit = handleSubmit(values => console.log(values));
</script>
<template>
<form @submit.prevent="submit" novalidate>
<label>Email <input v-model="email" /></label>
<span class="err">{{ emailErr }}</span>
<label>Password <input type="password" v-model="pass" /></label>
<span class="err">{{ passErr }}</span>
<label>Confirm <input type="password" v-model="confirm" /></label>
<span class="err">{{ confErr }}</span>
<button>Register</button>
</form>
</template>
What Just Happened?
useForm
shares context across fields—one edit triggers only relevant recalculations.validationSchema
centralizes business rules; translators change messages in one file.- Errors surface reactively as users type, with ARIA-live regions automatically announced.
Option 2: Rolling Your Own with ref
, computed
, and Native Constraint API
Sometimes a marketing micro-site doesn’t justify 12 kB of library code.
vueCopyEdit<script setup lang="ts">
import { ref, computed } from 'vue';
const email = ref('');
const password = ref('');
const confirm = ref('');
const errors = computed(() => {
const list: Record<string,string> = {};
if (!/.+@.+\..+/.test(email.value)) list.email = 'Invalid email';
if (password.value.length < 8) list.password = 'Min 8 chars';
if (password.value !== confirm.value) list.confirm = 'No match';
return list;
});
const valid = computed(() => Object.keys(errors.value).length === 0);
const submit = () => valid.value && console.log({ email, password });
</script>
<template>
<form @submit.prevent="submit" novalidate>
<input v-model="email" aria-invalid="email in errors">
<span class="err">{{ errors.email }}</span>
<input type="password" v-model="password" aria-invalid="password in errors">
<span class="err">{{ errors.password }}</span>
<input type="password" v-model="confirm" aria-invalid="confirm in errors">
<span class="err">{{ errors.confirm }}</span>
<button :disabled="!valid">Go</button>
</form>
</template>
Zero dependencies; fine for two or three fields.
Comparing the Two Approaches
Criteria | VeeValidate | Custom Reactive Rules |
---|---|---|
Bundle Size | ≈12 kB brotli | 0 kB |
Learning Curve | Declarative API | Native Vue only |
Complex Schemas | Yup/Zod integration, easy | Manual cross-field checks |
Localization | Built-in i18n hooks | DIY message maps |
Async Rules | validate() , automatic state | Manual Promise + loading state |
Dev Velocity | High once learned | Slower as rules grow |
Remote-Work Insight Box
While coding from a Panamanian cowork, our team added banking KYC fields that updated every quarter. VeeValidate’s schema let compliance tweak regex rules in a shared JSON, pushing to production without touching components. Devs surfing elsewhere simply pulled master, ran tests, and got back to hammocks—no merge hell.
Performance & Accessibility Checkpoints
- TBT — VeeValidate’s effect system is lazy; only dirty fields re-evaluate.
- CLS — reserve space for error spans or animate height with CSS transitions.
- Screen Readers — tie
<span role="alert">
to inputs viaaria-describedby
. - Mobile CPU — debounce expensive async checks (
v-async: 300ms
) to keep low-end Android smooth.
Common Pitfalls & Swift Fixes
Pitfall | Symptom | Fix |
---|---|---|
Schema redeclared in setup | Infinite validations on every render | Move schema outside component or to composable |
Async API spam | Debounce missing | Wrap fetch in useDebounceFn or VeeValidate’s validateOnBlur |
Password checker flickers | Using watcher instead of computed | Centralize logic in computed for atomic updates |
Error message translation drift | Hard-coded strings | Use i18n keys or VeeValidate locale files |
Code Snippet: Debounced Username Uniqueness (VeeValidate)
tsCopyEditconst { value: user, errorMessage: userErr } = useField<string>('username',
value => api.exists(value).then(exists => (!exists ? true : 'Taken')),
{ validateOnValueUpdate: false, validateOnBlur: true }
);
One declarative line vs. custom watchers + timers.
CLI Nuggets
Command | Purpose |
---|---|
vue add vee-validate | Vue-CLI plugin auto-registers components |
npm i zod @vee-validate/zod | Use Zod schemas instead of Yup |
npm run build --report | Check library footprint after tree-shaking |
Diagram (text)
Input event → VeeValidate field context → Validation schema → Error map → DOM updates via reactive binding → Screen reader alerts.
Wrap-Up
Choose VeeValidate when forms are many, rules complex, or non-devs tweak requirements. Reach for custom reactive solutions when pages are tiny and payload budgets razor-thin. Either way, lean on Vue.js reactivity, centralize logic, and honor accessibility to keep users—and investors—happy, whether they’re browsing on fibre in São Paulo or prepaid data in Santo Domingo.
Drop your validation war stories below; I’ll answer between airport layovers and late-night arepa sessions.