---
title: "Form state"
description: "Read reactive form and field state such as dirty, touched, valid, validating, and submitting, and avoid the common pitfalls."
canonical_url: "https://notformdocs.vercel.app/docs/guides/form-state"
---

> Index of every page on this site: [llms.txt](https://notformdocs.vercel.app/llms.txt)

State is exposed as plain reactive properties. Read only what your interface needs. Vue tracks those reads in templates and computed values, so there is nothing to subscribe to.

## Form state

<table>
<thead>
  <tr>
    <th>
      Property
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        isDirty
      </code>
    </td>
    
    <td>
      Current values differ (deep comparison) from the baseline <code>
        initialValues
      </code>
      
      . Reverting every change makes it <code>
        false
      </code>
      
       again.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isTouched
      </code>
    </td>
    
    <td>
      A field has blurred, or a submit has been attempted.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isSubmitted
      </code>
    </td>
    
    <td>
      At least one submit has been attempted, even an invalid one.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isSubmitting
      </code>
    </td>
    
    <td>
      Validation or the asynchronous <code>
        onSubmit
      </code>
      
       is running.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isValidating
      </code>
    </td>
    
    <td>
      A schema validation is running.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isValid
      </code>
    </td>
    
    <td>
      The current error list is empty. It does <strong>
        not
      </strong>
      
       run validation.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        submitCount
      </code>
    </td>
    
    <td>
      Number of submit attempts. Reset to <code>
        0
      </code>
      
       by <code>
        reset()
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        resetCount
      </code>
    </td>
    
    <td>
      Number of resets. Useful as a <code>
        :key
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        errors
      </code>
    </td>
    
    <td>
      The current list of issues.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        values
      </code>
    </td>
    
    <td>
      The live input values.
    </td>
  </tr>
</tbody>
</table>

```vue
<template>
  <Form :form="form">
    <p v-if="form.isDirty">
      You have unsaved changes.
    </p>

    <p
      v-if="form.isValidating"
      role="status"
    >
      Checking values...
    </p>

    <button
      type="submit"
      :disabled="form.isSubmitting"
    >
      {{ form.isSubmitting ? 'Saving...' : 'Save' }}
    </button>
  </Form>
</template>
```

## isValid describes errors, not values

`isValid` is `true` whenever `form.errors` is empty. A pristine form with invalid values reports `isValid === true` until something validates. Two consequences:

- Do not use `:disabled="!form.isValid"` to gate submission. A pristine but invalid form reports `true`, so the button stays enabled and signals nothing. Submitting is already safe, because `onSubmit` only runs when the schema passes.
- To get a fresh answer, call `await form.validate()`, or submit.

Prefer disabling the button with `isSubmitting`, and let validation show errors after the user acts.

## Field state

Every field instance exposes its own state.

<table>
<thead>
  <tr>
    <th>
      Property
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        errors
      </code>
    </td>
    
    <td>
      Issues reported at exactly this field's path.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isValid
      </code>
    </td>
    
    <td>
      <code>
        errors
      </code>
      
       is empty.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isDirty
      </code>
    </td>
    
    <td>
      The value differs (deep comparison) from the baseline value at this path.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isTouched
      </code>
    </td>
    
    <td>
      The field blurred, or a submit was attempted.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isValidating
      </code>
    </td>
    
    <td>
      The whole form, this field, or a field nested inside it is being validated.
    </td>
  </tr>
</tbody>
</table>

```vue
<template>
  <Field
    v-slot="{ field }"
    :field="form.field('username')"
  >
    <label :for="field.path">Username</label>

    <input
      v-bind="field.events"
      :id="field.path"
      v-model="field.model"
      type="text"
      autocomplete="username"
    >

    <span v-if="field.isValidating">Checking...</span>

    <FieldError :errors="field" />
  </Field>
</template>
```

### Array fields and nested paths

An array field has the same members, with these specifics:

- `isDirty` compares the **whole array**, so editing a nested item makes the array field dirty.
- `isValidating` includes validation of fields nested in its items.
- `errors` and `isValid` refer only to issues at the array's **own** path, such as a minimum item count. They do not include item errors.
- `isTouched` becomes `true` after a submit, or when the array field's own `onBlur` fires. It is not derived from item fields.

To ask whether any item has an error, check the issue list, as described in [Errors](https://notformdocs.vercel.app/docs/guides/errors#show-errors-for-a-section).

## Common patterns

### Unsaved changes warning

```ts
import { onBeforeRouteLeave } from 'vue-router'

onBeforeRouteLeave(() => {
  if (form.isDirty && !confirm('Discard unsaved changes?')) {
    return false
  }
})
```

### Disable Save until something changed

```vue
<template>
  <button
    type="submit"
    :disabled="!form.isDirty || form.isSubmitting"
  >
    Save
  </button>
</template>
```

### Show errors only after interaction

Fields do not validate until the user acts, so errors already appear at the right time. If you add your own early validation, for example with `validateOn: { input: true }`, you can still gate the display on touch:

```vue
<template>
  <FieldError
    v-if="field.isTouched"
    :errors="field"
  />
</template>
```

## Next steps

- [useForm](https://notformdocs.vercel.app/docs/composables/use-form) lists the complete public contract.
- [Field](https://notformdocs.vercel.app/docs/components/field) documents the field members.

## Sitemap

See the full [sitemap](https://notformdocs.vercel.app/sitemap.md) for all pages.
