Formify Docs

Examples

Practical examples of Formify in real-world scenarios

Examples

1. Signup form — the kitchen sink

Config schema + async unique check + password confirmation + i18n-safe messages + normalization:

import { useForm, uniqueCheck } from "formify-js";

interface SignupValues {
  username: string;
  email: string;
  password: string;
  confirm: string;
}

export function Signup() {
  const form = useForm<SignupValues>({
    initialValues: {
      username: "",
      email: "",
      password: "",
      confirm: "",
    },
    validationSchema: {
      username: {
        required: true,
        minLength: [3, "At least {min} characters"],
        alphanumeric: true,
        normalize: "trim",
        asyncValidate: uniqueCheck(
          async (value, { signal }) => {
            const res = await fetch(
              `/api/users/exists?username=${encodeURIComponent(String(value))}`,
              { signal },
            );
            return res.json();
          },
          { message: "This username is already taken", debounce: 400 },
        ),
      },
      email: { required: true, email: true, normalize: "trim" },
      password: { required: true, strongPassword: true },
      confirm: {
        required: true,
        confirm: "password",
        deps: ["password"], // re-checks when password changes
      },
    },
    onSubmit: async (values) => {
      await api.signup(values);
    },
  });

  return (
    <form onSubmit={form.handleSubmit} noValidate>
      <label>
        Username
        <input {...form.getFieldProps("username")} />
      </label>
      {form.validating.username && <p className="hint">Checking…</p>}
      {form.touched.username && form.errors.username && (
        <p className="error">{form.errors.username}</p>
      )}

      <label>
        Email
        <input {...form.getFieldProps("email")} type="email" />
      </label>
      {form.touched.email && form.errors.email && (
        <p className="error">{form.errors.email}</p>
      )}

      <label>
        Password
        <input {...form.getFieldProps("password")} type="password" />
      </label>
      {form.touched.password && form.errors.password && (
        <p className="error">{form.errors.password}</p>
      )}

      <label>
        Confirm password
        <input {...form.getFieldProps("confirm")} type="password" />
      </label>
      {form.touched.confirm && form.errors.confirm && (
        <p className="error">{form.errors.confirm}</p>
      )}

      <button type="submit" disabled={form.isSubmitting || !form.isValid}>
        {form.isSubmitting ? "Creating account…" : "Sign up"}
      </button>
    </form>
  );
}

2. Dynamic list with <FieldArray>

import { Formify, Form, Field, FieldArray, ErrorMessage } from "formify-js";

function InvoiceForm() {
  return (
    <Formify
      initialValues={{ lines: [{ description: "", amount: "" }] }}
      validationSchema={{
        lines: {}, // top-level; per-item rules live on Field validate below
      }}
      onSubmit={(values) => api.saveInvoice(values)}
    >
      <Form>
        <FieldArray name="lines">
          {({ push, remove, form }) => (
            <div>
              {form.values.lines.map((_line, index) => (
                <div key={index} className="row">
                  <Field
                    name={`lines.${index}.description`}
                    validate={(v) => (!v ? "Description is required" : undefined)}
                    placeholder="Description"
                  />
                  <Field
                    name={`lines.${index}.amount`}
                    validate={(v) =>
                      v !== "" && Number.isNaN(Number(v))
                        ? "Must be a number"
                        : undefined
                    }
                    placeholder="Amount"
                    type="number"
                  />
                  <button type="button" onClick={() => remove(index)}>✕</button>
                </div>
              ))}
              <button type="button" onClick={() => push({ description: "", amount: "" })}>
                Add line
              </button>
            </div>
          )}
        </FieldArray>
        <ErrorMessage name="lines" />
        <button type="submit">Save invoice</button>
      </Form>
    </Formify>
  );
}

3. Conditional fields

Only validate the coupon when the user opted in:

validationSchema: {
  applyCoupon: { oneOf: [true] },
  coupon: {
    required: true,
    pattern: /^SAVE\d{2}$/,
    when: (values) => values.applyCoupon === true,
    deps: ["applyCoupon"],
  },
}

deps: ["applyCoupon"] makes the coupon re-validate the moment the toggle changes.

4. Yup interop (no yup install required)

import * as yup from "yup"; // optional — formify duck-types it

const schema = yup.object({
  email: yup.string().email("Invalid email").required("Required"),
  age: yup.number().min(18, "Must be 18+"),
});

const form = useForm({
  initialValues: { email: "", age: "" },
  validationSchema: schema,
  onSubmit: (values) => api.save(values),
});

validateYupSchema / yupToFormErrors are also exported for advanced use.

5. i18n messages

const messages = {
  required: "Ce champ est obligatoire",
  email: "Veuillez saisir un email valide",
  minLength: "Au moins {min} caractères",
};

const form = useForm({
  initialValues: { email: "" },
  messages, // one object per locale — done
  validationSchema: { email: { required: true, email: true, minLength: 5 } },
  onSubmit: (values) => api.save(values),
});

6. Status + setFormifyState

Use status for server errors returned by the API:

const form = useForm({
  initialValues: { email: "" },
  onSubmit: async (values, { setStatus, setSubmitting }) => {
    try {
      await api.login(values);
    } catch (err) {
      setStatus(err.message);          // "Invalid credentials"
      setSubmitting(false);
    }
  },
});

{form.status && <div className="banner error">{form.status}</div>}

7. Formatters

Format values as the user types or on blur — see Formatters for the full list:

const form = useForm({
  initialValues: { ssn: "", phone: "", price: "", name: "" },
  validationSchema: {
    ssn:   { format: "ssn", ssn: true },
    phone: { format: { type: "phone", options: { country: "US", ext: true } }, phone: true },
    price: { format: { type: "currency", trigger: "onBlur" }, decimal: true },
    name:  { format: [{ type: "trim", trigger: "onBlur", options: { mode: "both" } }, "upperCase"] },
  },
  onSubmit: (values) => api.save(values),
});

8. Ready-made plugins

const plugins = [
  autosavePlugin({ save: (values) => api.saveDraft(values), debounce: 800 }),
  honeypotPlugin(),
  confirmDirtyPlugin(),
  passwordStrengthPlugin(),
];

See Plugins for each one.

9. Live examples + benchmark

The repo ships a Vite test app with every example above running live, plus a performance benchmark page that measures per-keystroke latency, full-form validation and submit cost at 10 / 50 / 100 / 250 fields:

cd app && npm install && npm run dev

On this page