# useNotForm

> Create a type-safe form instance with schema validation, reactive state, field tracking, and submission handling.

`useNotForm` is the core composable for creating and managing a NotForm instance.

It connects a Standard Schema-compatible validator to a deeply reactive form state and provides everything needed to manage values, validation, field interaction, errors, submission, and reset behavior.

The returned form instance can be used directly or provided to [`<NotForm>`](/docs/components/not-form) for use by descendant field components.

## API

### Signature

```ts
function useNotForm<TSchema extends ObjectSchema>(
  config: UseNotFormConfig<TSchema>
): NotFormAPI<TSchema>
```

### Parameters

<field-group>
<field name="config" type="UseNotFormConfig<TSchema>" :required="true">

Schema, initial values/errors, and submit handler configuration.

<br />

<br />


The configuration options are:

<field-group>
<field name="schema" type="MaybeRefOrGetter<TSchema>" :required="true">

Schema used to parse and validate values.

<br />

<br />



It can be provided directly, as a ref to a schema or as a function that returns a schema.

```ts
import { z } from 'zod'

const schema = z.object({
  email: z.email(),
})

const form = useNotForm({
  schema,
})
```

<caution>

The schema must be Standard Schema compatible whose input is an object.

</caution>
</field>

<field name="initialValues" type="DeepPartial<InferInput<TSchema>>">

Starting or initial values for the form.

<br />

<br />



The supplied values become the form's initial baseline for form values and are used for:

- populating `form.values`
- dirty state comparisons
- `form.reset()`

```ts
const form = useNotForm({
  schema,
  initialValues: {
    email: 'user@example.com',
    profile: {
      name: 'Jane',
    },
  },
})
```

<note>

When `initialValues` is omitted, the form starts from an empty object.

</note>
</field>

<field name="initialErrors" type="Array<Issue>">

Issues shown before the first validation run.

<br />

<br />



This is useful when a form needs to start with errors returned from another source, such as previously persisted validation state.

<br />

<br />



The supplied values become the form's initial baseline for validation errors and are used for:

- populating `form.errors`
- dirty state comparisons
- `form.reset()`

```ts
const form = useNotForm({
  schema,
  initialValues: {
    email: 'user@example.com',
  },
  initialErrors: [{ message: 'Email already taken', path: 'email' }],
})
```

<note>

When `initialErrors` is omitted, the form starts with no issues.

</note>
</field>

<field name="onSubmit" type="(data: InferOutput<TSchema>) => Promise<void> | void">

Callback function that is called after a successful submit validation.

```ts
const form = useNotForm({
  schema,
  initialValues: {
    email: 'user@example.com',
  },
  onSubmit(values) {
    console.log(values)
  },
})
```

</field>
</field-group>
</field>
</field-group>

### Return Value

`useNotForm` returns a `NotFormAPI<TSchema>` object, which is a reactive form instance with the following properties:

<field-group>
<field name="errors" type="Array<Issue>">

Issues from the last validation that wrote errors.

<br />

<br />



Each issue contains the path and message supplied by the validation schema.

</field>

<field name="getFieldErrors" type="(path: Paths<TSchema>) => Array<Issue>">

Returns all active validation issues whose path exactly equals `path` — not issues nested underneath it. `getFieldErrors('groups.0')` won't include an issue reported at `groups.0.name`. See [NotArrayField](/docs/components/not-array-field) for how `isValid` accounts for that instead, by aggregating recursively rather than doing an exact match.

</field>

<field name="isDirty" type="boolean">

Indicates if the form values have been changed since the form was initialized.

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

</field>

<field name="isSubmitting" type="boolean">

Indicates if the form is currently submitting.

```vue
<template>
  <button
    type="submit"
    :disabled="form.isSubmitting"
  >
    {{ form.isSubmitting ? 'Submitting...' : 'Submit' }}
  </button>
</template>
```

</field>

<field name="isTouched" type="boolean">

Indicates if any form field has been touched.

```vue
<template>
  <div>
    <p v-if="form.isTouched">
      You have interacted with at least one field.
    </p>
  </div>
</template>
```

</field>

<field name="isValid" type="boolean">

Indicates if the form currently has no active validation errors.

```vue
<template>
  <button
    type="submit"
    :disabled="!form.isValid"
  >
    Submit
  </button>
</template>
```

<note>

`isValid` is based on the current `errors` collection. It does not independently execute validation.

</note>
</field>

<field name="isValidating" type="boolean">

Indicates if the form is currently being validated.

```vue
<template>
  <div>
    <p v-if="form.isValidating">
      Validating...
    </p>
  </div>
</template>
```

</field>

<field name="reset" type="(values?: DeepPartial<InferInput<TSchema>>, errors?: Array<Issue>) => void">

Restores values/errors to the baseline. Optional arguments become the new baseline.

<br />

<br />



This also resets the dirty, validation, and touch states.

- **Reset with new values:**

```ts
form.reset({
  email: 'jane@example.com',
  name: 'Jane',
})
```

- **Reset with new errors:**

```ts
form.reset(
  undefined,
  [
    {
      message: 'Email requires verification.',
      path: ['email'],
    },
  ]
)
```

- **Reset with both new values and errors:**

```ts
form.reset(
  {
    email: 'jane@example.com',
    name: 'Jane',
  },
  [
    {
      message: 'Email requires verification.',
      path: ['email'],
    },
  ]
)
```

- **Reset without new values and errors:**

```ts
form.reset()
```

<note>

Calling `form.reset()` restores the form to its current baseline. If that baseline has since been updated, `reset()` restores to the updated values and errors. Otherwise, it falls back to the original `initialValues` and `initialErrors`, or to an empty state if neither was provided.

</note>

<caution>

Passing `values`/`errors` fully **replaces** the previous baseline — it isn't merged with it. Any key present in the old baseline but missing from the new `values` is simply gone afterward, the same way `initialValues` passed to `useNotForm` becomes the literal starting shape rather than a set of defaults layered under something else.

</caution>
</field>

<field name="setError" type="(error: Issue) => void">

Upserts an issue at the same path, or appends it.

<br />

<br />



Use this when you need to manually manage validation issues for a field, for example, after a server-side validation.

<code-preview>
<demos-native-server-errors>



</demos-native-server-errors>

<template v-slot:code="">
<code-collapse>
<docs-demo-code file="native/server-errors">



</docs-demo-code>
</code-collapse>
</template>
</code-preview>
</field>

<field name="setValue" type="<TPath extends Paths<TSchema>>(path: TPath, value: Get<InferInput<TSchema>, TPath, { strict: false }>) => void">

Sets a field value by path.

<br />

<br />



Use this when you need to manually set the value of a field, for example, after a server-side update or custom inputs.

```ts
form.setValue('email', 'jane@example.com')
```

<note>

Calling `setValue()` updates the field's value and recomputes `isDirty` by comparing the new value against the current baseline.

</note>

<caution>

`setValue()` does not trigger validation on its own. Validation still runs only through the field's configured interaction handlers (e.g. `onChange`, `onBlur`), so call `validate()` manually if you need to validate right after a programmatic `setValue()` call.

</caution>
</field>

<field name="submit" type="(event?: SubmitEvent) => Promise<void>">

Marks all fields as touched and dirty, validates the complete form, then runs `onSubmit` when valid.

<br />

<br />



- **Bind it directly to <NotForm>**:

```vue
<template>
  <NotForm
    :form="form"
    @submit="form.submit"
  >
    <input v-model="form.values.email">

    <button
      type="submit"
      :disabled="form.isSubmitting"
    >
      {{ form.isSubmitting ? 'Submitting...' : 'Submit' }}
    </button>
  </NotForm>
</template>
```

- **Or call programmatically:**

```ts
form.submit()
```

<note>

When called with an `event`, `event.preventDefault()` is called immediately — before checking whether a submission is already in flight, and before validation runs. This is why binding `@submit="form.submit"` is enough on its own to stop the browser's default full-page submission; see [NotForm](/docs/components/not-form) for the full distinction between how `submit` and `reset` are each prevented.

</note>

<note>

If no `onSubmit` callback was provided, the form still runs validation on submit but has no callback to execute when it succeeds.

</note>
</field>

<field name="validate" type="() => Promise<StandardSchemaV1.Result<InferOutput<TSchema>>>">

Validates the entire form against the current schema.

<br />

<br />



On success, the form's errors are cleared and the schema's validated (typed) output is returned. On failure, the returned result contains the validation issues, and those same issues replace the form's current errors.

<note>

Concurrent calls are last-write-wins: each call still resolves with its own result, but only the most recently started call gets to write its issues to `form.errors` — an older call that resolves later has its result silently discarded from shared state.

</note>
</field>

<field name="validateField" type="(path: Paths<TSchema>) => Promise<StandardSchemaV1.Result<InferOutput<TSchema>>>">

Validates a specific field against the current schema.

<br />

<br />



On success, the field's validated (typed) output is returned. On failure, the returned result contains the validation issues, and those same issues are added to the form's current errors.

<br />

<br />



`path` can be any granularity — a leaf field, an entire array field, or a specific item inside one. This is useful when building custom inputs that need to validate themselves without validating the entire form; `<NotField>` and `<NotArrayField>` both call this internally with their own `path`.

</field>

<field name="values" type="InferInput<TSchema>">

Deeply reactive object containing the live form values.

<br />

<br />



It can be accessed directly:

```ts
console.log(form.values.email)
```

Or used for two-way binding with v-model:

```vue
<template>
  <input v-model="form.values.email">
</template>
```

```vue
<template>
  <input v-model="form.values.address.city">
</template>
```

</field>
</field-group>
