Form State
Track the current state of your form with the reactive formState object.
Accessing Form State
const { formState } = useForm({ schema })
// Access with .value (it's a ref)
console.log(formState.value.isSubmitting)
console.log(formState.value.errors)Available Properties
errors
Object containing validation errors for each field:
formState.value.errors
// { email?: string, password?: string, ... }<template>
<span v-if="formState.value.errors.email">
{{ formState.value.errors.email }}
</span>
</template>isDirty
true if any field's current value differs from its default value.
The dirty state uses value comparison, not event tracking:
- Setting a value equal to the default does NOT mark the field dirty
- Reverting a field to its default value automatically clears its dirty state
- Arrays and objects are compared by their serialized value (deep equality)
formState.value.isDirty // boolean<template>
<button type="submit" :disabled="!formState.value.isDirty">Save Changes</button>
</template>TIP
Use isDirty to enable save buttons only when actual changes exist. Users can freely edit and revert without the form appearing "changed".
isValid
true if the form has no validation errors:
formState.value.isValid // booleanTIP
isValid is true whenever the form has no validation errors, regardless of whether the user has interacted with any fields. This matches React Hook Form behavior.
isSubmitting
true while the form is being submitted:
formState.value.isSubmitting // boolean<template>
<button type="submit" :disabled="formState.value.isSubmitting">
{{ formState.value.isSubmitting ? 'Submitting...' : 'Submit' }}
</button>
</template>isSubmitted
true after the form has been submitted at least once:
formState.value.isSubmitted // booleanUseful for showing errors only after first submission attempt.
isSubmitSuccessful
true if the last submission was successful (no errors):
formState.value.isSubmitSuccessful // booleansubmitCount
Number of times the form has been submitted:
formState.value.submitCount // numbertouchedFields
Record of fields that have been interacted with (blurred):
formState.value.touchedFields // Record<string, boolean>
// Check if specific field was touched
formState.value.touchedFields['email'] // boolean
// or
formState.value.touchedFields.email // boolean<template>
<!-- Only show error after user interacts with field -->
<span v-if="formState.value.touchedFields['email'] && formState.value.errors.email">
{{ formState.value.errors.email }}
</span>
</template>dirtyFields
Record of fields whose current values differ from their default values.
Fields are added when their value differs from the default, and removed when reverted to match the default:
formState.value.dirtyFields // Record<string, boolean>
// Check if specific field is dirty
formState.value.dirtyFields['email'] // boolean
// or
formState.value.dirtyFields.email // booleanisLoading
true while async default values are being fetched:
formState.value.isLoading // boolean<template>
<div v-if="formState.value.isLoading">Loading form data...</div>
<form v-else>...</form>
</template>isReady
true once the form is fully initialized (inverse of isLoading):
formState.value.isReady // booleandefaultValuesError
Contains the error if async default values failed to load:
formState.value.defaultValuesError // unknown | undefined<template>
<div v-if="formState.value.defaultValuesError" class="error">
Failed to load form data. Please refresh.
</div>
</template>isValidating
true if any field is currently being validated (useful for async validation):
formState.value.isValidating // boolean<template>
<button :disabled="formState.value.isValidating">
{{ formState.value.isValidating ? 'Validating...' : 'Submit' }}
</button>
</template>validatingFields
Set of fields currently being validated:
formState.value.validatingFields // Set<string><template>
<input v-bind="register('username')" />
<span v-if="formState.value.validatingFields.has('username')"> Checking availability... </span>
</template>disabled
true if the form is disabled:
formState.value.disabled // booleanGetting Individual Field State
Use getFieldState to get the state of a specific field:
const { getFieldState } = useForm({ schema })
const emailState = getFieldState('email')
// Returns: { isDirty: boolean, isTouched: boolean, invalid: boolean, error?: string }<script setup>
const emailState = computed(() => getFieldState('email'))
</script>
<template>
<div>
<input v-bind="register('email')" />
<span v-if="emailState.value.isTouched && emailState.value.error">
{{ emailState.value.error }}
</span>
</div>
</template>Common Patterns
Conditional Submit Button
<template>
<button type="submit" :disabled="!formState.value.isDirty || formState.value.isSubmitting">
{{ formState.value.isSubmitting ? 'Saving...' : 'Save' }}
</button>
</template>Unsaved Changes Warning
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
const { formState } = useForm({ schema })
// Warn before leaving with unsaved changes
const beforeUnloadHandler = (e: BeforeUnloadEvent) => {
if (formState.value.isDirty) {
e.preventDefault()
e.returnValue = ''
}
}
onMounted(() => {
window.addEventListener('beforeunload', beforeUnloadHandler)
})
onBeforeUnmount(() => {
window.removeEventListener('beforeunload', beforeUnloadHandler)
})
</script>Show Errors After Submission
<template>
<div>
<input v-bind="register('email')" />
<!-- Only show error after form has been submitted -->
<span v-if="formState.value.isSubmitted && formState.value.errors.email">
{{ formState.value.errors.email }}
</span>
</div>
</template>Success Message
<template>
<div v-if="formState.value.isSubmitSuccessful" class="success">Form submitted successfully!</div>
<form v-else @submit="handleSubmit(onSubmit)">
<!-- Form fields -->
</form>
</template>Progress Indicator
<template>
<div class="form-wrapper">
<div v-if="formState.value.isSubmitting" class="loading-overlay">
<span class="spinner"></span>
<p>Submitting...</p>
</div>
<form @submit="handleSubmit(onSubmit)">
<!-- Form fields -->
</form>
</div>
</template>Resetting Form State
The reset function clears all form state:
const { reset } = useForm({ schema })
// Reset to default values
reset()
// Reset to new values
reset({
email: 'new@example.com',
name: 'New Name',
})After reset:
- All values return to defaults (or provided values)
errorsis clearedisDirtybecomesfalsetouchedFieldsis cleareddirtyFieldsis clearedsubmitCountremains unchanged
Next Steps
- Learn about Submission handling
- Explore Error Handling patterns
