Formify Docs

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.

On this page