This page introduces the basic concepts and terminology used in the @tanstack/vue-form library. Familiarizing yourself with these concepts will help you better understand and work with the library.
You can create options for your form so that it can be shared between multiple forms by using the formOptions function.
const formOpts = formOptions({
defaultValues: {
firstName: '',
lastName: '',
hobbies: [],
} as Person,
})
A Form Instance is an object that represents an individual form and provides methods and properties for working with the form. You create a form instance using the useForm composable.
const form = useForm({
...formOpts,
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
})
Standalone
You may also create a form instance without using formOptions:
const form = useForm({
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
defaultValues: {
firstName: '',
lastName: '',
hobbies: [],
} as Person,
})
Field
A Field represents a single form input element, such as a text input or a checkbox. Fields are created using the form.Field component provided by the form instance. The component accepts a name prop and uses a scoped slot via the v-slot directive.
<template>
<form.Field name="fullName">
<template v-slot="{ field }">
<input
:name="field.name"
:value="field.state.value"
@blur="field.handleBlur"
@input="(e) => field.handleChange(e.target.value)"
/>
</template>
</form.Field>
</template>
Field State
Each field has its own state, which includes its current value, validation status, error messages, and other metadata. You can access a field’s state using the field.state property.
const {
value,
meta: { errors, isValidating },
} = field.state
There are four states in the metadata that track how the user interacts with a field:
- isTouched - Set after the user changes the field or blurs the field
- isDirty - Set after the field’s value has been changed, even if it’s been reverted to the default (opposite of
isPristine)
- isPristine - Remains true until the user changes the field value (opposite of
isDirty)
- isBlurred - Set after the field has been blurred
const { isTouched, isDirty, isPristine, isBlurred } = field.state.meta
Understanding ‘isDirty’
TanStack Form uses a persistent ‘dirty’ state model:
- A field remains ‘dirty’ once changed, even if reverted to the default value
- This is similar to Angular Form and Vue FormKit
- Different from React Hook Form, Formik, and Final Form (which use non-persistent dirty state)
To support non-persistent dirty state, use the isDefaultValue flag:
const { isDefaultValue, isTouched } = field.state.meta
// Re-create non-persistent dirty functionality
const nonPersistentIsDirty = !isDefaultValue
Field API
The Field API is an object provided by a scoped slot using the v-slot directive. This slot receives an argument named field that provides methods and properties for working with the field’s state.
<template v-slot="{ field }">
<input
:name="field.name"
:value="field.state.value"
@blur="field.handleBlur"
@input="(e) => field.handleChange(e.target.value)"
/>
</template>
Validation
@tanstack/vue-form provides both synchronous and asynchronous validation out of the box. Validation functions can be passed to the form.Field component using the validators prop.
<template>
<form.Field
name="firstName"
:validators="{
onChange: ({ value }) =>
!value
? `A first name is required`
: value.length < 3
? `First name must be at least 3 characters`
: undefined,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return value.includes('error') && 'No "error" allowed in first name'
},
}"
>
<template v-slot="{ field }">
<input
:value="field.state.value"
@input="(e) => field.handleChange(e.target.value)"
@blur="field.handleBlur"
/>
<FieldInfo :field="field" />
</template>
</form.Field>
</template>
Standard Schema Libraries
TanStack Form supports the Standard Schema specification. You can define a schema using any of these libraries:
- Zod (v3.24.0 or higher)
- Valibot (v1.0.0 or higher)
- ArkType (v2.1.20 or higher)
- Yup (v1.7.0 or higher)
<script setup lang="ts">
import { useForm } from '@tanstack/vue-form'
import { z } from 'zod'
const form = useForm({
// ...
})
const onChangeFirstName = z.string().refine(
async (value) => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return !value.includes('error')
},
{
message: "No 'error' allowed in first name",
},
)
</script>
<template>
<form.Field
name="firstName"
:validators="{
onChange: z.string().min(3, 'First name must be at least 3 characters'),
onChangeAsyncDebounceMs: 500,
onChangeAsync: onChangeFirstName,
}"
>
<template v-slot="{ field, state }">
<label :htmlFor="field.name">First Name:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
@input="(e) => field.handleChange((e.target as HTMLInputElement).value)"
@blur="field.handleBlur"
/>
<FieldInfo :state="state" />
</template>
</form.Field>
</template>
Reactivity
@tanstack/vue-form offers various ways to subscribe to form and field state changes, most notably the form.useStore method and the form.Subscribe component.
Using useStore
<script setup lang="ts">
const firstName = form.useStore((state) => state.values.firstName)
</script>
Using Subscribe Component
<template>
<form.Subscribe>
<template v-slot="{ canSubmit, isSubmitting }">
<button type="submit" :disabled="!canSubmit">
{{ isSubmitting ? '...' : 'Submit' }}
</button>
</template>
</form.Subscribe>
</template>
The usage of the form.useField method to achieve reactivity is discouraged since it is designed to be used thoughtfully within the form.Field component. Use form.useStore instead.
Listeners
@tanstack/vue-form allows you to react to specific triggers and “listen” to them to dispatch side effects.
<template>
<form.Field
name="country"
:listeners="{
onChange: ({ value }) => {
console.log(`Country changed to: ${value}, resetting province`)
form.setFieldValue('province', '')
},
}"
>
<template v-slot="{ field }">
<input
:value="field.state.value"
@input="(e) => field.handleChange(e.target.value)"
/>
</template>
</form.Field>
</template>
Array Fields
Array fields allow you to manage a list of values within a form, such as a list of hobbies. You can create an array field using the form.Field component with the mode="array" prop.
When working with array fields, you can use these methods:
pushValue - Add a value to the end of the array
removeValue - Remove a value at a specific index
swapValues - Swap two values at different indices
moveValue - Move a value from one index to another
insertValue - Insert a value at a specific index
replaceValue - Replace a value at a specific index
clearValues - Clear all values in the array
<template>
<form @submit.prevent.stop="form.handleSubmit">
<form.Field name="hobbies" mode="array">
<template v-slot="{ field: hobbiesField }">
<div>
Hobbies
<div>
<div
v-if="
Array.isArray(hobbiesField.state.value) &&
!hobbiesField.state.value.length
"
>
No hobbies found.
</div>
<div v-else>
<div v-for="(_, i) in hobbiesField.state.value" :key="i">
<form.Field :name="`hobbies[${i}].name`">
<template v-slot="{ field }">
<div>
<label :for="field.name">Name:</label>
<input
:id="field.name"
:name="field.name"
:value="field.state.value"
@blur="field.handleBlur"
@input="(e) => field.handleChange(e.target.value)"
/>
<button
type="button"
@click="hobbiesField.removeValue(i)"
>
X
</button>
<FieldInfo :field="field" />
</div>
</template>
</form.Field>
</div>
</div>
<button
type="button"
@click="
hobbiesField.pushValue({
name: '',
description: '',
yearsOfExperience: 0,
})
"
>
Add hobby
</button>
</div>
</div>
</template>
</form.Field>
</form>
</template>
These are the basic concepts and terminology used in the @tanstack/vue-form library. Understanding these concepts will help you work more effectively with the library and create complex forms with ease.