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