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> for use by descendant field components.

API

Signature

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

Parameters

config
UseNotFormConfig<TSchema> required
Schema, initial values/errors, and submit handler configuration.

The configuration options are:
schema
MaybeRefOrGetter<TSchema> required
Schema used to parse and validate values.

It can be provided directly, as a ref to a schema or as a function that returns a schema.
import { z } from 'zod'

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

const form = useNotForm({
  schema,
})
The schema must be Standard Schema compatible whose input is an object.
initialValues
DeepPartial<InferInput<TSchema>>
Starting or initial values for the form.

The supplied values become the form's initial baseline for form values and are used for:
  • populating form.values
  • dirty state comparisons
  • form.reset()
const form = useNotForm({
  schema,
  initialValues: {
    email: 'user@example.com',
    profile: {
      name: 'Jane',
    },
  },
})
When initialValues is omitted, the form starts from an empty object.
initialErrors
Array<Issue>
Issues shown before the first validation run.

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

The supplied values become the form's initial baseline for validation errors and are used for:
  • populating form.errors
  • dirty state comparisons
  • form.reset()
const form = useNotForm({
  schema,
  initialValues: {
    email: 'user@example.com',
  },
  initialErrors: [{ message: 'Email already taken', path: 'email' }],
})
When initialErrors is omitted, the form starts with no issues.
onSubmit
(data: InferOutput<TSchema>) => Promise<void> | void
Callback function that is called after a successful submit validation.
const form = useNotForm({
  schema,
  initialValues: {
    email: 'user@example.com',
  },
  onSubmit(values) {
    console.log(values)
  },
})

Return Value

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

errors
Array<Issue>
Issues from the last validation that wrote errors.

Each issue contains the path and message supplied by the validation schema.
getFieldErrors
(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 for how isValid accounts for that instead, by aggregating recursively rather than doing an exact match.
isDirty
boolean
Indicates if the form values have been changed since the form was initialized.
<template>
  <div>
    <p v-if="form.isDirty">
      You have unsaved changes.
    </p>
  </div>
</template>
isSubmitting
boolean
Indicates if the form is currently submitting.
<template>
  <button
    type="submit"
    :disabled="form.isSubmitting"
  >
    {{ form.isSubmitting ? 'Submitting...' : 'Submit' }}
  </button>
</template>
isTouched
boolean
Indicates if any form field has been touched.
<template>
  <div>
    <p v-if="form.isTouched">
      You have interacted with at least one field.
    </p>
  </div>
</template>
isValid
boolean
Indicates if the form currently has no active validation errors.
<template>
  <button
    type="submit"
    :disabled="!form.isValid"
  >
    Submit
  </button>
</template>
isValid is based on the current errors collection. It does not independently execute validation.
isValidating
boolean
Indicates if the form is currently being validated.
<template>
  <div>
    <p v-if="form.isValidating">
      Validating...
    </p>
  </div>
</template>
reset
(values?: DeepPartial<InferInput<TSchema>>, errors?: Array<Issue>) => void
Restores values/errors to the baseline. Optional arguments become the new baseline.

This also resets the dirty, validation, and touch states.
  • Reset with new values:
form.reset({
  email: 'jane@example.com',
  name: 'Jane',
})
  • Reset with new errors:
form.reset(
  undefined,
  [
    {
      message: 'Email requires verification.',
      path: ['email'],
    },
  ]
)
  • Reset with both new values and errors:
form.reset(
  {
    email: 'jane@example.com',
    name: 'Jane',
  },
  [
    {
      message: 'Email requires verification.',
      path: ['email'],
    },
  ]
)
  • Reset without new values and errors:
form.reset()
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.
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.
setError
(error: Issue) => void
Upserts an issue at the same path, or appends it.

Use this when you need to manually manage validation issues for a field, for example, after a server-side validation.
setValue
<TPath extends Paths<TSchema>>(path: TPath, value: Get<InferInput<TSchema>, TPath, { strict: false }>) => void
Sets a field value by path.

Use this when you need to manually set the value of a field, for example, after a server-side update or custom inputs.
form.setValue('email', 'jane@example.com')
Calling setValue() updates the field's value and recomputes isDirty by comparing the new value against the current baseline.
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.
submit
(event?: SubmitEvent) => Promise<void>
Marks all fields as touched and dirty, validates the complete form, then runs onSubmit when valid.

  • Bind it directly to <NotForm>:
<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:
form.submit()
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 for the full distinction between how submit and reset are each prevented.
If no onSubmit callback was provided, the form still runs validation on submit but has no callback to execute when it succeeds.
validate
() => Promise<StandardSchemaV1.Result<InferOutput<TSchema>>>
Validates the entire form against the current schema.

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.
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.
validateField
(path: Paths<TSchema>) => Promise<StandardSchemaV1.Result<InferOutput<TSchema>>>
Validates a specific field against the current schema.

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.

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.
values
InferInput<TSchema>
Deeply reactive object containing the live form values.

It can be accessed directly:
console.log(form.values.email)
Or used for two-way binding with v-model:
<template>
  <input v-model="form.values.email">
</template>
<template>
  <input v-model="form.values.address.city">
</template>