Building Reusable Form Fields with VeeValidate 4's Composition API
Learn how to turn repetitive VeeValidate validation into reusable, type‑safe composables with useField and useForm in Vue 3.
03 Sept 2025, 16:40 UTC

Problem: Repetitive validation logic slows down form development
When you build multiple Vue 3 forms, you often copy‑paste the same validation rules, error handling, and touched state for each input. This duplication makes templates noisy, obscures the intent of the form, and makes it harder to keep validation logic in sync across components.
Thesis: VeeValidate 4’s composables let you extract field logic into reusable, type‑safe utilities while keeping the template clean
The useField and useForm composables expose reactive validation state directly in the setup function. By wrapping a field’s logic in a composable, you can reuse it across forms and still benefit from VeeValidate’s automatic error messages and TypeScript inference.
1. Creating a reusable field with useField
Start by defining a composable that returns everything a template needs to bind an input:
import { useField } from '@vee-validate/vue4';
import * as yup from 'yup';
export function useEmailField() {
const { value, errorMessage, touched, handleBlur, handleChange } = useField(
'email',
yup.string().email('Please enter a valid email').required('Email is required')
);
return {
value,
errorMessage,
touched,
blur: handleBlur,
input: handleChange,
};
}
The composable uses Yup for declarative rules, but you could pass a function or a string rule instead. The returned object contains:
value– the current input value (reactive)errorMessage– validation error string or undefinedtouched– boolean that becomes true after the first blurhandleBlurandhandleChange– callbacks to attach to@blurand@input
Because the composable is a plain function, TypeScript infers the types of value and errorMessage from the Yup schema, giving you autocomplete and compile‑time safety.
2. Assembling a form with useForm
Now build a login form that reuses the email field and adds a password field:
import { useForm } from '@vee-validate/vue4';
import { useEmailField } from './useEmailField';
import * as yup from 'yup';
export default {
setup() {
const { value: email, errorMessage: emailError, touched: emailTouched, blur: emailBlur, input: emailInput } = useEmailField();
const { value: password, errorMessage: passwordError, touched: passwordTouched, handleBlur: passwordBlur, handleChange: passwordInput } = useField(
'password',
yup.string().min(8, 'Password must be at least 8 characters').required('Password is required')
);
const { handleSubmit, resetForm } = useForm({
// initial values can be omitted; they default to empty strings
onSubmit: (values) => {
// values is { email: string, password: string } with correct types
alert(`Submitted: ${JSON.stringify(values)}`);
},
});
return {
email,
emailError,
emailTouched,
emailBlur,
emailInput,
password,
passwordError,
passwordTouched,
passwordBlur,
passwordInput,
handleSubmit,
resetForm,
};
},
};
The template stays minimal:
<form @submit.prevent="handleSubmit">
<label>
Email
<input
v-model="email"
@blur="emailBlur"
@input="emailInput"
/>
<span v-if="emailTouched" class="error">{{ emailError }}</span>
</label>
<label>
Password
<input
type="password"
v-model="password"
@blur="passwordBlur"
@input="passwordInput"
/>
<span v-if="passwordTouched" class="error">{{ passwordError }}</span>
</label>
<button type="submit">Login</button>
</form>
When the user types, the reactive value updates; on blur, touched becomes true and errorMessage shows any validation failure. Submitting via handleSubmit only calls onSubmit when all fields pass validation.
3. Trade‑offs and limitations
- Learning effort: Teams used to VeeValidate 3’s
ValidationProvidercomponents must rewrite markup and get comfortable with the composable API. - Runtime overhead: Each composable creates additional reactive proxies. In forms with hundreds of fields this can slightly increase update cost, though the impact is usually negligible for typical UI sizes.
- Yup is optional: If you prefer custom validator functions, ensure they are pure and return either
trueor a string error message; otherwise VeeValidate may treat the result incorrectly.
4. Actionable closing
Start small: extract one frequently used field (e.g., email, phone number) into a composable and replace its occurrences in existing forms. Verify the behavior by opening Vue Devtools, inspecting the reactive objects returned by useField, and confirming that errorMessage updates as you interact with the input. Once comfortable, compose larger forms with useForm and enjoy type‑safe, reusable validation logic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.