If you build forms in React, you've probably tried Formik, React Hook Form, or final-form. They're good. But if you deal with complex, deeply nested, multi-step forms — the kind that make you question your life choices — there's a better tool.
mobx-react-form combines MobX reactivity with a powerful form state engine. Here's why it deserves your attention.
1. Nested Fields Without the Pain
Real forms aren't flat. Take an invoice: you have a customer section, a shipping address, and a variable number of products, each with name, quantity, and price.
MRF handles this natively:
const fields = [
'customer.name',
'customer.email',
'shipping.address',
'shipping.city',
'shipping.zip',
'products',
'products[].name',
'products[].quantity',
'products[].unitPrice',
];
Each path becomes an observable field with its own validation, errors, and dirty tracking. No flattening. No normalization. The values serialize back to exactly the structure your API expects:
{
"customer": { "name": "...", "email": "..." },
"shipping": { "address": "...", "city": "...", "zip": "..." },
"products": [
{ "name": "Widget A", "quantity": 2, "unitPrice": 19.99 }
]
}
And with v6.15, errors bubble up automatically:
const form = new MobxReactForm({ fields }, {
options: { bubbleUpErrorMessages: true }
});
// When products[0].product is empty, this just works:
{form.error && <ErrorBanner message={form.error} />}
2. Validation Plugins — Bring Your Own
MRF doesn't lock you into one validator. It supports 6 validation plugins out of the box:
| Plugin | Package | Style |
|---|---|---|
| DVR | validatorjs | Declarative rules (`'required |
| VJF | Custom functions | {% raw %}(value) => [isValid, message]
|
| ZOD | zod | Schema-based |
| YUP | yup | Schema-based |
| JOI | joi | Schema-based |
| SVK | ajv | Schema-based |
Use DVR for simple fields and ZOD for complex cross-field validation — in the same form:
plugins() {
return {
dvr: dvr({ package: validatorjs }),
zod: zodPlugin({ package: z, schema: invoiceSchema }),
};
}
Define a Zod schema for your entire form and get both client-side validation and TypeScript types from one source:
const invoiceSchema = z.object({
customer: z.object({
name: z.string().min(2),
email: z.string().email(),
}),
products: z.array(z.object({
name: z.string().min(1),
quantity: z.number().min(1),
unitPrice: z.number().positive(),
})).min(1),
});
3. Full TypeScript Generics (v6.13)
If you're on TypeScript, MRF now offers end-to-end type safety:
interface InvoiceForm {
customer: { name: string; email: string };
shipping: { address: string; city: string; zip: string };
products: Array<{ name: string; quantity: number; unitPrice: number }>;
}
const form = new MobxReactForm<InvoiceForm>({ fields });
form.$('products[0]').$('name'); // Field<string> — fully typed
form.values(); // InvoiceForm — fully typed
form.$('customer').$('name').set('Acme Corp'); // type-safe setter
Plus PathsOf<T> for autocomplete:
form.$('ship') // → editor suggests: 'shipping.address' | 'shipping.city' | 'shipping.zip'
No more guessing field paths. No more runtime errors from typos.
4. Dynamic & Array Fields
Need to add products dynamically? Remove a shipping address from a list? Built in.
// Add a product
form.$('products').add({
product: 'New Product',
quantity: 1,
unitPrice: 0,
});
// Remove the second product
form.$('products').del(1);
// Reorder (new in v6.14 with ArrayMap)
form.$('products').move(0, 2);
Arrays, nested arrays, dynamic field creation — MRF handles all of it with MobX reactivity. Each item gets its own validation lifecycle, dirty tracking, and error state.
5. Composer — Multi-Form Orchestration
Have independent forms that need to submit together? The composer() function coordinates validation across forms:
const checkout = composer({ billing, shipping, payment });
checkout.validate().then(({ valid, errors, values }) => {
if (valid) {
submitOrder({
...values.billing,
shipping: values.shipping,
payment: values.payment,
});
} else {
// errors is keyed by form name: { billing: {...}, shipping: {...}, payment: {...} }
showValidationSummary(errors);
}
});
Perfect for checkout flows, multi-tab dashboards, and wizard-style UIs where each step is an independent form.
6. UI Library Agnostic
MRF works with every major UI library — and doesn't force you to pick one:
- Material UI
- Ant Design
- React Aria
- Headless UI
- React Widgets
- React-Select
- Plain HTML
Each gets a thin binding layer. The form logic stays the same whether you render with MUI TextFields or raw <input> elements.
// Works the same regardless of UI library
<form onSubmit={form.onSubmit}>
<Input field={form.$('customer.name')} />
<Input field={form.$('customer.email')} />
<button type="submit">Save</button>
</form>
Swap the Input component from MaterialTextField to AntdInput to SimpleInput — the form state doesn't care.
7. Built for Production, Maintained for Years
MRF has been powering production forms for 8+ years. It has 1.1k+ GitHub stars, and the latest releases (June 2026) brought the most requested features:
- v6.15 — Error bubbling in nested forms
- v6.14 — ArrayMap with predictable ordering and drag-and-drop support
- v6.13 — Full TypeScript generics, strict null checks, path autocomplete
-
v6.12 — Custom validation type inference, YUP
.ref()support - v6.11 — Full null values support
Each release was driven by real production needs, not hypothetical use cases.
The Bottom Line
| Scenario | MRF | Alternatives |
|---|---|---|
| Simple login form | ✅ Overkill but works | ✅ Great fit |
| Deeply nested fields (invoice, order, profile) | ✅ Native support | ❌ Requires flattening |
| Dynamic arrays (add/remove/reorder) | ✅ Built-in | ❌ Manual state mgmt |
| Multi-form validation (checkout, wizard) | ✅ Composer | ❌ Custom orchestration |
| Validation plugins (DVR/ZOD/YUP/JOI) | ✅ 7 plugins | ❌ Usually 1-2 |
| TypeScript generics + path autocomplete | ✅ v6.13+ | ❌ Limited |
| Error bubbling in nested forms | ✅ v6.15+ | ❌ Manual recursion |
| UI library flexibility | ✅ Any library | ✅ Any library |
If you build forms beyond a login page — invoices, checkouts, multi-step registrations, admin panels with dynamic sections — give MRF a try. It handles the complexity so you don't have to.
npm install --save mobx-react-form
Top comments (0)