Form Basics
Form is a reactive form management system built on MobX that tracks form state, validation, and submission lifecycle. It automatically manages dirty state, field-level tracking, and nested form hierarchies, making it easy to build complex forms with proper validation and user feedback.
It leverages @mobx-sentinel/core for dirty state tracking (Watcher) and validation (Validator).
Getting a Form Instance
Section titled “Getting a Form Instance”Use Form.get() to retrieve or create a form instance for an object:
const model = new MyModel();const form = Form.get(model);Form instances are cached. The same object always returns the same form instance:
const form1 = Form.get(model);const form2 = Form.get(model);// form1 === form2 (same instance)A form is garbage collected together with its subject, unless the subject's Watcher or Validator keeps the subject alive, as Getting a Watcher Instance and Getting a Validator Instance describe. The fields of a form observe the validation state, that of nested objects included, only while a report waits for the validation to settle.
⚠️ Note: Form.get() starts change tracking immediately because it creates a Watcher instance as part of the form initialization process. See Starting a Watcher for details.
Multiple Forms Per Subject
Section titled “Multiple Forms Per Subject”Use a symbol key to maintain multiple independent forms for the same object:
const editFormKey = Symbol('edit');const previewFormKey = Symbol('preview');
const editForm = Form.get(model, editFormKey);const previewForm = Form.get(model, previewFormKey);// editForm !== previewForm (different instances)Nested/Array Forms
Section titled “Nested/Array Forms”Forms automatically track sub-forms when using the @nested annotation:
class Address { @observable street = ''; @observable city = '';}
class User { @observable name = ''; @nested @observable address = new Address(); @nested @observable previousAddresses = [new Address()];}
const user = new User();const userForm = Form.get(user);
// Reach a sub-form through the property it lives on.// A form is cached per subject and form key, so with the same key this is the// instance the parent tracks.const addressForm = Form.get(user.address);const prevAddressForm = Form.get(user.previousAddresses[0]);
// Or walk what the parent tracks. `subForms` is an iterator, not a map:// each entry carries the annotated member's name, its key path and the form.// (a hoisted member's `key` is `KeyPath.Self` rather than the name it is declared with)for (const { key, keyPath } of userForm.subForms) { console.log(key, keyPath); // "address" "address" // "previousAddresses" "previousAddresses.0"}
Array.from(userForm.subForms, (entry) => entry.data);// [addressForm, prevAddressForm]When sub-forms become dirty, parent forms automatically become dirty too. This allows validation and dirty checking to bubble up through the form hierarchy.
Form State
Section titled “Form State”Forms provide several reactive state properties:
// Dirty state - whether the form has changesform.isDirty; // boolean
// Validation stateform.isValid; // booleanform.invalidFieldCount; // number of invalid fieldsform.invalidFieldPathCount; // includes nested formsform.isValidating; // boolean - async validation in progress
// Submission stateform.isSubmitting; // booleanform.isBusy; // true if submitting or validating
// Combined stateform.canSubmit; // true if ready to submitSubmission Readiness
Section titled “Submission Readiness”canSubmit checks if the form can be submitted based on:
- Not currently busy (submitting or validating)
- Valid (unless
allowSubmitInvalidis enabled)
Whether the form is dirty doesn't matter, so a form with pre-filled values can be submitted as it is.
Error Handling
Section titled “Error Handling”The validator tracks errors from the start, but form.getErrors() and the bindings expose a field's errors only after they have been reported. See Smart Error Reporting for when that happens.
// Field-specific errorsform.getErrors('email'); // Set<string> - empty until reportedform.getErrors('city', 'postalCode'); // several fields, each once it is reported
// Regardless of reportingform.getField('email').errors; // Set<string>form.validator.getErrorMessages('**'); // every error, those of nested forms includedform.validator.getErrorMessages('address.**'); // the address field and the nested address form
// First error message, regardless of reportingform.firstErrorMessage; // string | nullReport errors to make them visible:
// Report errors on all fields and sub-formsform.reportError();
// e.g., when a submission failsform.addHandler('didSubmit', (succeed) => { if (!succeed) form.reportError();});Managing State
Section titled “Managing State”// Mark form as dirtyform.markAsDirty();
// Reset form state (clears dirty, fields, sub-forms)form.reset();
// Note: reset() does NOT clear validation errors// Errors are managed by the Validator and remain until revalidationmarkAsDirty(), reset() and reportError() take no parameters and are bound to the form, so they can be passed as event handlers as they are:
<button onClick={form.reset}>Reset</button>