Skip to content

Form State ​

Track the current state of your form with the reactive formState object.

Accessing Form State ​

typescript
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:

typescript
formState.value.errors
// { email?: string, password?: string, ... }
vue
<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)
typescript
formState.value.isDirty // boolean
vue
<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:

typescript
formState.value.isValid // boolean

TIP

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:

typescript
formState.value.isSubmitting // boolean
vue
<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:

typescript
formState.value.isSubmitted // boolean

Useful for showing errors only after first submission attempt.

isSubmitSuccessful ​

true if the last submission was successful (no errors):

typescript
formState.value.isSubmitSuccessful // boolean

submitCount ​

Number of times the form has been submitted:

typescript
formState.value.submitCount // number

touchedFields ​

Record of fields that have been interacted with (blurred):

typescript
formState.value.touchedFields // Record<string, boolean>

// Check if specific field was touched
formState.value.touchedFields['email'] // boolean
// or
formState.value.touchedFields.email // boolean
vue
<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:

typescript
formState.value.dirtyFields // Record<string, boolean>

// Check if specific field is dirty
formState.value.dirtyFields['email'] // boolean
// or
formState.value.dirtyFields.email // boolean

isLoading ​

true while async default values are being fetched:

typescript
formState.value.isLoading // boolean
vue
<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):

typescript
formState.value.isReady // boolean

defaultValuesError ​

Contains the error if async default values failed to load:

typescript
formState.value.defaultValuesError // unknown | undefined
vue
<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):

typescript
formState.value.isValidating // boolean
vue
<template>
  <button :disabled="formState.value.isValidating">
    {{ formState.value.isValidating ? 'Validating...' : 'Submit' }}
  </button>
</template>

validatingFields ​

Set of fields currently being validated:

typescript
formState.value.validatingFields // Set<string>
vue
<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:

typescript
formState.value.disabled // boolean

Getting Individual Field State ​

Use getFieldState to get the state of a specific field:

typescript
const { getFieldState } = useForm({ schema })

const emailState = getFieldState('email')
// Returns: { isDirty: boolean, isTouched: boolean, invalid: boolean, error?: string }
vue
<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 ​

vue
<template>
  <button type="submit" :disabled="!formState.value.isDirty || formState.value.isSubmitting">
    {{ formState.value.isSubmitting ? 'Saving...' : 'Save' }}
  </button>
</template>

Unsaved Changes Warning ​

vue
<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 ​

vue
<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 ​

vue
<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 ​

vue
<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:

typescript
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)
  • errors is cleared
  • isDirty becomes false
  • touchedFields is cleared
  • dirtyFields is cleared
  • submitCount remains unchanged

Next Steps ​

Released under the MIT License.