Validation and errors
validateOn / reValidateOn, rules, default controller actions, and the error tree conventions.
1) Two-phase auto validation: validateOn / reValidateOn
Form splits auto validation into two phases by default:
- Before first submit:
validateOncontrols whether validation runs ononChange/onBlur(default: validate only on submit) - After first submit:
reValidateOncontrols whether incremental validation runs ononChange/onBlur(default:onChange)
This avoids “heavy validation on every keystroke” for large forms/lists, while still giving immediate feedback after the user’s first submit attempt.
2) Rules: treat deps as a contract
Prefer writing validation in rules (field/list/root rules). deps is the contract for “linkage-triggered validation”: only when you explicitly declare dependencies will changes in related fields trigger this rule’s incremental validation.
See Rules DSL (z) for the
rulesDSL (const z = $.rules).
const $ = Form.from(Values)
const z = $.rules
const ProfileForm = Form.make("ProfileForm", {
values: Values,
initialValues: { name: "" },
rules: z(
z.field("name", {
// deps defaults to []: declare only when you need cross-field linkage triggers
required: "Required",
minLength: 2,
}),
),
})When a rule needs to read other fields inside the same object and you want “other field changes also trigger this field’s validation”, add deps (e.g. deps: ["preferredChannel"]).
2.1) $self: object-level refine (don’t overwrite subtree)
For object-level cross-field validation, prefer writing errors to errors.<path>.$self (instead of overwriting the whole errors.<path> subtree):
rules: z(
z.field(
"profile.security",
{
deps: ["password", "confirmPassword"],
validate: (security: any) =>
security?.password === security?.confirmPassword ? undefined : "Passwords do not match",
},
{ errorTarget: "$self" },
),
)2.2) List validation: list/item scopes
- Item-level (item): return
{ field: error }or{ $item: error }, written toerrors.<list>.rows[i].* - List-level (list): return
{ $list: error }or{ rows: [...] }, written toerrors.<list>.$list/errors.<list>.rows[i]
3) Form.Rule.*: organizing rules and reuse
z.field/z.list/z.root: recommended declarative entry points (types narrow by values schema)Form.Rule.make(...): low-level normalization utility (expand config into a rule set attachable tocheck)Form.Rule.merge(...): low-level merge utility (duplicate ruleName fails deterministically)- Built-in validators:
required/minLength/maxLength/min/max/pattern
4) Default controller actions (triggerable outside components)
The controller returned by useForm(formBlueprint) is consistent across React and Logic. You can trigger form actions outside components:
controller.validate(): root validate (includes Schema writes)controller.validatePaths(paths): precise validation by valuePath (field or list)controller.setError(path, error)/controller.clearErrors(paths?): write/clear manual errorscontroller.reset(values?): reset values/errors/ui/$formcontroller.handleSubmit({ onValid, onInvalid? }): submit flow (increment submitCount, validate, branch by errorCount)
Also: if you dispatch the submit action directly (or call form.submit()), it only triggers Rules root validation. Schema validation only runs in controller.validate() / controller.handleSubmit(...).
Tip: if UI wants to validate only a small path (e.g. one row in a field array), prefer
validatePathsto avoid pulling the whole form into one validation transaction.
5) traits: kept as an advanced entry
You can still use traits to express lower-level StateTrait structures, but it’s recommended to keep “regular validation” in rules and reserve traits for:
- computed / link / source (derivations, linkage, async resources)
- a small number of advanced cases that require direct node/list manipulation (paired with performance/diagnostic comparisons)
[!TIP] For a broader mental model and the semantic boundaries around “transaction windows / convergence”: