Common Pitfalls

Every entry here is silent: nothing throws, nothing fails to type-check, the UI just doesn't do what you'd expect. Each one links to the guide that covers the mechanism in full; this page is only the "watch out for this" index.

Watching in the same component that owns the form

Calling useWatch in the same component that calls useForm re-renders the whole form on every change, exactly what useForm not subscribing by itself is meant to avoid.

Extract the subscription and the UI it drives into their own component instead. See Reactivity.

disabled cascades through state, not through the DOM

Setting form.disabled = true reaches every registered field's FieldApi.disabled, but nothing on screen changes until a field's own component actually reads it. A TextField that never reads field.disabled leaves its <input> editable no matter what disabled says.

Combine the field's own disabled with a disabled prop the caller can also set (disabled={disabled || field.disabled} in React, this.disabled || field.disabled in Lit) so both an ancestor cascade and a one-off override work. See Basic and Submission Handling.

Reassigning validators/dependents doesn't revalidate by itself

Passing a fresh validators array on every render (a validator factory like required() returns a new closure each call) doesn't trigger a new validation run on its own — only an actual value change or an explicit validate(true) does. A field can look like it's still validating against its old rules if you're watching for some other signal that the option "took effect."

This is deliberate, not a bug: reacting to the reference change alone would turn every render into a validation run. Cache the array yourself (useMemo in React, a class field in Lit) if you want reassigning the same set to be a true no-op. See Per-node Validation.

Reading a sibling's value inside a validator isn't a dependency

A validator can read any other field's value directly (form.value.password), but that read isn't tracked. Without declaring dependents, editing password won't re-run confirmPassword's validator, leaving a stale "Passwords must match" error until confirmPassword is next edited or blurred itself.

See Linked Fields.

Index as a list key

Keying an array's rendered rows on their index, instead of the item's own id, misattributes uncontrolled DOM state (focus, cursor position, scroll) to the wrong row after a reorder: the item that renders at index 2 changes, but the component/element instance React/Lit reuses for index 2 doesn't.

Use FieldApi.id as the key instead. See Dynamic Arrays.

handleSubmit doesn't move the dirty baseline

A successful onSubmit doesn't reset anything on its own: dirty (and the reset baseline it's computed from) stay exactly where they were before you submitted, so a form that just saved successfully still reports dirty: true.

Call form.reset() or form.reset(saved) to reset the baseline. See Dirty Tracking & Reset.

field.id in server-rendered markup

INFO

React only — there's no SSR-oriented guide for the Lit binding in this repo yet.

field.id is a stable, module-level counter: good for a React key since it survives array reorders, but not seeded the same way on the server and the client. Rendering it into an actual DOM attribute (an <input id={field.id}> / <label htmlFor={field.id}> pair, say) mismatches during hydration. Use useId() for a DOM id instead, and keep field.id scoped to key.

See Server-Side Rendering (SSR) for the full guide.