use-form
Fully typed form state with validation, field arrays, and submission.
| Package | @vizejs/composable/use-form |
| Own the source | vize lib pull composable:use-form |
| Runtime exports | useForm |
| Gzip budget | 7168 B |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/composable
Import the entry directly. Call lifecycle-bound utilities inside <script setup> so cleanup follows the component scope; consult the runtime contract for this entry's SSR and hydration behavior.
import { useForm } from "@vizejs/composable/use-form";
Try it
Validate a workshop registration preview
Validate two related fields together and create a local preview only after both validators pass.
What to observe: Enter Al and an incomplete email. Errors wait until Preview registration; editing after that attempt revalidates each field. Use Ada and [email protected] to create the preview. Reset registration clears inputs, errors, the submit count, and the preview.
Destructure field refs before binding them with v-model. handleSubmit prevents native form submission, validates every field, and calls the local preview callback only when valid. novalidate lets the composable's recorded errors explain these example rules. valid means no errors are currently recorded, so this example does not treat an untouched form as validated. The submit count includes rejected attempts. reset() restores initial form values and state; the example separately clears its local preview. No request or registration is sent. Setup and SSR start with unvalidated values; owning-scope disposal aborts pending validation.
Compare the initial and interacted states


Copy the example
Save this complete SFC in your Vue project. Its public imports and source are the same ones executed in the preview.
<script setup lang="ts">
import { ref, useId } from "vue";
import { useForm } from "@vizejs/composable/use-form";
const registrationId = useId();
const lastPreview = ref("");
const { field, dirty, submitCount, submitting, handleSubmit, reset } = useForm({
initialValues: { name: "", email: "" },
validateOn: "submit",
revalidateOn: "change",
validators: {
name: (value) => (value.trim().length >= 3 ? undefined : "Use at least three characters."),
email: (value) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
? undefined
: "Enter an email address with a domain.",
},
});
const { value: name, error: nameError, onBlur: blurName } = field("name");
const { value: email, error: emailError, onBlur: blurEmail } = field("email");
const previewRegistration = handleSubmit((values) => {
lastPreview.value = `Last preview: ${values.name.trim()} · ${values.email}`;
});
function resetRegistration(): void {
reset();
lastPreview.value = "";
}
</script>
<template>
<form class="composable-example" novalidate @submit="previewRegistration">
<p>Create a local workshop registration preview. No details are sent.</p>
<label :for="`${registrationId}-name`">Attendee name</label>
<input
:id="`${registrationId}-name`"
v-model="name"
name="name"
autocomplete="name"
:aria-invalid="Boolean(nameError)"
:aria-describedby="`${registrationId}-name-help ${registrationId}-name-error`"
@blur="blurName"
/>
<p :id="`${registrationId}-name-help`">Use at least three characters.</p>
<p :id="`${registrationId}-name-error`" role="alert">{{ nameError || "" }}</p>
<label :for="`${registrationId}-email`">Contact email</label>
<input
:id="`${registrationId}-email`"
v-model="email"
name="email"
type="email"
autocomplete="email"
:aria-invalid="Boolean(emailError)"
:aria-describedby="`${registrationId}-email-help ${registrationId}-email-error`"
@blur="blurEmail"
/>
<p :id="`${registrationId}-email-help`">Use an address such as [email protected].</p>
<p :id="`${registrationId}-email-error`" role="alert">{{ emailError || "" }}</p>
<output aria-live="polite"
>Changed: {{ dirty ? "yes" : "no" }} · Submit attempts: {{ submitCount }}</output
>
<p role="status">{{ lastPreview || "No registration preview yet." }}</p>
<div class="example-actions">
<button type="submit" :disabled="submitting">Preview registration</button>
<button type="button" @click="resetRegistration">Reset registration</button>
</div>
<p>
Validation starts with Preview registration. After that attempt, edits revalidate each field.
</p>
</form>
</template>
Runtime contract
| Utility | Category | Stability | SSR | Hydration | Cleanup | Targets | Host globals | Uses |
|---|---|---|---|---|---|---|---|---|
useForm |
state | experimental | safe | stable | reactive-scope | web, server, worker, native, desktop, terminal | AbortController, DOMException, structuredClone |
tryOnScopeDispose, validateStandardSchema |
API
useForm
Fully typed form state with validation, field arrays, and submission. The value type is inferred from initialValues; every path is a template-literal key, so field("address.city") returns controls whose value is typed as the city's type and misspelled paths do not compile. Validation combines a Standard Schema v1 validator (Zod, Valibot, ArkType, … via the ~standard protocol), a whole-form validator, and per-field validators; all may be asynchronous. A newer validation aborts older ones through their signal and their results are discarded, so a slow check can never overwrite fresher errors. This composable owns the values. @vizejs/ui/form owns accessible error presentation (error summaries, focus management, native constraint validation); feed FormControls.errors into it to render them. Server rendering: pure state, no host access; validation never runs automatically during setup, so server and client render the same markup. Pending validations are aborted when the owning scope stops.
function useForm<Values extends object, Output = Values>( options: UseFormOptions<Values, Output>, ): FormControls<Values, Output>
const form = useForm({
initialValues: { name: "", address: { city: "" }, tags: [] as string[] },
validators: { name: (name) => (name ? undefined : "Required") },
onSubmit: (values) => api.save(values),
});
const city = form.field("address.city"); // city.value: Ref<string>
const tags = form.fieldArray("tags");
tags.append("vue");
Types
FieldValidatorContext
Context passed to field validators.
| Member | Type | Description |
|---|---|---|
values |
Values |
Current form values (undefined for standalone fields). |
path |
string |
Dotted path of the validated field. |
signal |
AbortSignal |
Aborted when a newer validation supersedes this one. |
UseFormOptions
Options for useForm.
| Member | Type | Description |
|---|---|---|
initialValues |
Values | (() => Values) |
Initial values, or a factory producing fresh initial values. |
schema? |
StandardSchemaV1<NoInfer<Values>, Output> |
Standard Schema v1 validator for the whole form. Its output becomes the submitted value. |
validate? |
NoInfer<FormValidator<Values>> |
Plain whole-form validator. |
validators? |
NoInfer<FormFieldValidators<Values>> |
Plain per-field validators keyed by path. |
validateOn? |
FormValidationTrigger |
When fields validate before the first submit. |
revalidateOn? |
FormValidationTrigger |
When fields re-validate after the first submit. |
onSubmit? |
(value: Output, context: { readonly signal: AbortSignal }) => unknown |
Default submit handler for FormControls.submit. |
SetFieldValueOptions
Options for FormControls.setValue.
| Member | Type | Description |
|---|---|---|
touch? |
boolean |
Mark the field touched. |
validate? |
boolean |
Validate the field after setting it. |
FormFieldControls
Reactive controls for one field, returned by FormControls.field.
| Member | Type | Description |
|---|---|---|
path |
Path |
Dotted path of the field (also a suitable name attribute). |
value |
WritableComputedRef<Value> |
Writable value bound to the form state (use with v-model). |
errors |
ComputedRef<readonly string[]> |
Current error messages. |
error |
ComputedRef<string | undefined> |
First error message. |
dirty |
ComputedRef<boolean> |
Whether the value differs from its initial value. |
touched |
ComputedRef<boolean> |
Whether the field was blurred or touched programmatically. |
validating |
ComputedRef<boolean> |
Whether a validation of this field is pending. |
onBlur |
() => void |
Mark the field touched (wire to blur); validates when configured. |
validate |
() => Promise<boolean> |
Validate this field. |
reset |
() => void |
Restore the initial value and clear errors and touched state. |
FormArrayEntry
One entry of a field array, with a stable key for v-for.
| Member | Type | Description |
|---|---|---|
key |
number |
Stable key that follows the item through moves. |
index |
number |
Current index. |
path |
|
Dotted path of the item, e.g. items.2. |
value |
Item |
Current item value. |
FormFieldArrayControls
Typed operations on an array field, returned by FormControls.fieldArray.
| Member | Type | Description |
|---|---|---|
entries |
ComputedRef<readonly FormArrayEntry<Item, Path>[]> |
Entries with stable keys. |
append |
(...items: Item[]) => void |
Append items. |
prepend |
(...items: Item[]) => void |
Prepend items. |
insert |
(index: number, item: Item) => void |
Insert an item at index. |
remove |
(index: number) => void |
Remove the item at index, shifting errors and touched state. |
move |
(from: number, to: number) => void |
Move an item from one index to another, carrying its state along. |
swap |
(first: number, second: number) => void |
Swap two items. |
replace |
(items: readonly Item[]) => void |
Replace all items and reset their state. |
FormControls
Reactive state and actions returned by useForm.
| Member | Type | Description |
|---|---|---|
values |
Ref<Values> |
Current values. Mutations are tracked (dirty state derives from them). |
errors |
Readonly<ShallowRef<FormErrorRecord>> |
Current errors keyed by path; "" holds form-level errors. |
valid |
ComputedRef<boolean> |
Whether no errors are currently recorded. |
dirty |
ComputedRef<boolean> |
Whether any value differs from its initial value. |
validating |
ComputedRef<boolean> |
Whether any validation is pending. |
submitting |
Readonly<Ref<boolean>> |
Whether a submission is in progress. |
submitCount |
Readonly<Ref<number>> |
Number of submit attempts. |
field |
<Path extends FormPath<Values>>( path: Path, ) => FormFieldControls<FormPathValue<Values, Path>, Path> |
Typed controls for one field. |
fieldArray |
<Path extends FormArrayPath<Values>>( path: Path, ) => FormFieldArrayControls<FormArrayItem<Values, Path>, Path> |
Typed operations for an array field. |
getValue |
<Path extends FormPath<Values>>(path: Path) => FormPathValue<Values, Path> |
Read the value at a path. |
setValue |
<Path extends FormPath<Values>>( path: Path, value: FormPathValue<Values, Path>, options?: SetFieldValueOptions, ) => void |
Write the value at a path. |
setValues |
(values: Partial<Values>, options?: { readonly merge?: boolean }) => void |
Replace (or shallowly merge) the values without touching initial values. |
isTouched |
(path: FormPath<Values>) => boolean |
Whether a field was touched. |
setErrors |
(errors: FormErrors<Values>) => void |
Replace errors (for example with server-side errors). |
clearErrors |
(path?: FormPath<Values> | "") => void |
Clear errors of one path, or all errors. |
validate |
() => Promise<FormResult<Output>> |
Run every validator, aborting any older validation still in flight. |
submit |
( handler?: (value: Output, context: { readonly signal: AbortSignal }) => unknown, ) => Promise<FormResult<Output>> |
Validate and, when valid, call handler (or the onSubmit option). A newer submit aborts an older one's pending validation. |
handleSubmit |
( handler?: (value: Output, context: { readonly signal: AbortSignal }) => unknown, ) => (event?: Event) => Promise<FormResult<Output>> |
Create a submit event listener that prevents the native submission. |
reset |
(values?: Values) => void |
Restore initial values (or install new ones) and clear all state. |