Best Practices
Recommended patterns for building forms with Formify
Best Practices
Recommended patterns for building robust, maintainable forms with Formify.
Validation Design
1. Start with config schemas
Config-driven schemas are plain data — easy to read, serialize, and reuse across forms:
validationSchema: {
email: { required: true, email: true },
password: { required: true, minLength: 8, strongPassword: true },
}Use a validate function only for cross-field rules that no single field config can express, and combine both freely.
2. Remember empty-value semantics
Only required fails on empty input. Every other rule validates the value when present — so optional fields work without extra ceremony. When a field must be filled, always combine the rule with required: true.
3. Use confirm + deps for password confirmation
validationSchema: {
password: { required: true, minLength: 8 },
confirm: { required: true, confirm: "password", deps: ["password"] },
}deps re-validates the confirm field whenever password changes — no manual wiring.
4. Conditional validation with when + deps
validationSchema: {
applyCoupon: { oneOf: [true] },
coupon: {
required: true,
pattern: /^SAVE\d{2}$/,
when: (values) => values.applyCoupon === true,
deps: ["applyCoupon"],
},
}5. Centralize messages for i18n
Keep one message dictionary per locale and pass it to every form:
const messages = {
required: "Ce champ est obligatoire",
minLength: "Au moins {min} caractères",
};
useForm({ initialValues, messages, validationSchema, onSubmit });Per-field overrides still win when a single field needs a special message.
Performance
6. Use <FastField> for large forms
<Field> re-renders on every form change. For forms with dozens of fields, use the memoized <FastField> so only the fields whose values change re-render.
7. Memoize plugins
Create ready-made plugins (e.g. autosavePlugin) once at module scope or inside useMemo — the plugin object identity matters to the form.
8. Debounce async checks
Always set a debounce (per field or globally via validateDebounce) before hitting an API:
asyncValidateOptions: { debounce: 400 }
// or
useForm({ validateDebounce: 300, ... })Debounce is bypassed on submit, so submission is never delayed.
UX
9. Surface async loading state
Show per-field spinners with form.validating.<field> and form-wide spinners with form.isValidating — users should know a uniqueness check is in flight.
10. Normalize and format before validating
normalize keeps values clean while typing (e.g. normalize: "trim"); format applies masks (SSN, phone, currency) on change or blur. Both run before validation, so errors always match what the user sees.
11. Focus the first error
Keep focusFirstError: true (the default) so after a failed submit the user lands on the first invalid field instead of hunting for it.
Security
12. Re-check hard requirements server-side
Async validator failures are treated as valid (a network hiccup never blocks the user). For hard requirements — uniqueness, verification — re-verify on the server during onSubmit, or use a plugin's onSubmit hook to gate submission with an API call.
13. Add a honeypot for public forms
plugins: [honeypotPlugin()]A hidden field humans never fill silently blocks bots that do.
14. Guard against data loss
plugins: [confirmDirtyPlugin({ message: "You have unsaved changes — leave anyway?" })]Warn before leaving the page while the form is dirty, and auto-save drafts with autosavePlugin for long forms.