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

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?


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

CriteriaVeeValidateCustom Reactive Rules
Bundle Size≈12 kB brotli0 kB
Learning CurveDeclarative APINative Vue only
Complex SchemasYup/Zod integration, easyManual cross-field checks
LocalizationBuilt-in i18n hooksDIY message maps
Async Rulesvalidate(), automatic stateManual Promise + loading state
Dev VelocityHigh once learnedSlower 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


Common Pitfalls & Swift Fixes

PitfallSymptomFix
Schema redeclared in setupInfinite validations on every renderMove schema outside component or to composable
Async API spamDebounce missingWrap fetch in useDebounceFn or VeeValidate’s validateOnBlur
Password checker flickersUsing watcher instead of computedCentralize logic in computed for atomic updates
Error message translation driftHard-coded stringsUse 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

CommandPurpose
vue add vee-validateVue-CLI plugin auto-registers components
npm i zod @vee-validate/zodUse Zod schemas instead of Yup
npm run build --reportCheck 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.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x