21

Core concepts

The mental model behind NotForm: the schema, the form instance, fields, paths, the initial-values baseline, and what happens on every keystroke and submit.

Everything in NotForm follows from a handful of ideas. Read this page once and the rest of the documentation becomes predictable.

The schema is the source of truth

You pass an object schema to useForm. NotForm derives three things from it:

  • the type of form.values, which is the schema's input;
  • the paths that form.field() and form.arrayField() accept;
  • the type of the data that onSubmit receives, which is the schema's output.

The schema also performs all validation. NotForm has no rule syntax of its own.

Input and output can differ. A schema that turns the text '42' into the number 42 has input { age: string } and output { age: number }. Inputs bind to the values users edit. Outputs are what your code receives. See Schema input and output.

One form instance owns the state

useForm returns a reactive form instance. It owns:

  • values: the live, editable data;
  • errors: the current list of validation issues;
  • state flags such as isDirty, isSubmitting, and isValidating;
  • actions such as submit, validate, reset, setError, and setValue.

You read state directly (form.isDirty, not form.isDirty.value) and pass the instance to <Form :form="form">.

Fields are handles, not copies

form.field('email') does not store a value. It returns a handle whose model reads and writes form.values.email, and whose errors, isTouched, isDirty, and events are scoped to that path.

Fields are cached by path. Calling form.field('email') again, including on every render, returns the same instance with the same id. That is why touched state and pending validation survive re-renders.

Consequence: the optional configuration passed to form.field(path, config) is read once, on the first call for that path.

Field is a convenience component that hands a field to its slot. It is optional. A field is also usable straight from script.

Paths are strings; issue paths are segments

You address values with dot-notation paths: 'email', 'address.city', 'contacts.0.email'. Paths are checked against the schema input.

Validators report where an issue occurred as an array of segments: ['address', 'city']. When you set errors yourself, you write segments:

form.setError({ message: 'Already taken.', path: ['username'] })

An issue with no path applies to the whole form.

Initial values are a baseline

initialValues and initialErrors define the baseline. The form compares against it to compute isDirty, and reset() restores it. reset(values, errors) replaces the baseline, which is how you load saved data into an edit form.

What happens when a user types

  1. The input updates field.model, which writes into form.values.
  2. The input's event (input, change, or blur) calls the matching handler in field.events.
  3. The handler checks the field's configuration: is this event enabled, or is the field in eager mode with an existing error?
  4. If a debounce is configured, validation waits for a pause. Blur never waits.
  5. NotForm validates a snapshot of the whole form with your schema.
  6. Only the issues at this field's exact path replace the field's errors.
  7. If the user changed any value while validation was running, the result is discarded as stale and the errors are left alone.

Details are in Validation.

What happens when a user submits

  1. If a submit is already running, the new one is ignored.
  2. isSubmitting becomes true and submitCount increases. Every field now counts as touched.
  3. The whole form is validated.
  4. If a value changed while validating, validation runs again.
  5. If the schema reports issues, they replace all errors and onSubmit is not called.
  6. If it passes, errors are cleared and onSubmit is awaited with the validated output.
  7. isSubmitting returns to false, even if onSubmit throws.

Details are in Submission and reset.

Glossary

TermMeaning
Schema inputThe shape of form.values: what users edit, before parsing.
Schema outputThe shape onSubmit receives: the parsed result.
PathA dot-notation string such as 'address.city'. Used with form.field() and form.setValue().
SegmentsA path as an array, such as ['address', 'city']. Used by issues.
IssueOne validation problem: a message and an optional path.
Field instanceThe cached handle returned by form.field(path).
Array fieldA field for an array, with items and mutation methods. Returned by form.arrayField(path).
ItemOne entry of an array field. It is a field for its own value and has an index.
BaselineThe current initialValues and initialErrors.
TouchedA field was blurred, or a submit was attempted.
DirtyCurrent values differ from the baseline.
StaleA validation result that was made obsolete by a newer run or a value change. It is discarded.

Next steps

Copyright © 2026 NotForm
Published under MIT License