Async Validation
Debounced, cancellable async validation and unique checks
Async Validation
Formify makes async validation (API calls, uniqueness checks, server-side format validation) a first-class feature:
- Debounced — configurable per field or globally
- Cancellable — stale responses are aborted via
AbortSignaland ignored - Per-field loading flags —
form.validating.username - Submit-safe — async validators always complete before
onSubmitruns
Basic async validator
const form = useForm({
initialValues: { username: "" },
validationSchema: {
username: {
required: true,
asyncValidate: async (value, values, { signal }) => {
const res = await fetch(`/api/validate-username?value=${value}`, { signal });
const data = await res.json();
return data.valid ? undefined : data.message;
},
asyncValidateOptions: { debounce: 400 },
},
},
onSubmit: (values) => api.createUser(values),
});- The validator receives
(value, values, ctx)wherectx.signalaborts when the field changes again or the form unmounts. - Return an error message, or
undefined/nullwhen valid. debouncewaits for the user to pause typing before hitting the API.
uniqueCheck — "is this already taken?"
The most common async check is uniqueness. uniqueCheck wraps a fetcher that returns true when the value is taken:
import { useForm, uniqueCheck } from "formify-js";
const form = useForm({
initialValues: { username: "" },
validationSchema: {
username: {
required: true,
minLength: 3,
asyncValidate: uniqueCheck(
async (value, { signal }) => {
const res = await fetch(
`/api/users/exists?username=${encodeURIComponent(String(value))}`,
{ signal },
);
return res.json(); // true = already exists
},
{ message: "This username is already taken", debounce: 400 },
),
},
},
onSubmit: (values) => api.createUser(values),
});
// In the UI:
{form.validating.username && <Spinner size="sm" />}Cancellation in action
Type "joh", pause, type "john":
- The check for "joh" is aborted the moment the value changes.
- Its result (even if the network resolves later) is discarded.
- Only the result for "john" is applied.
You get this for free — pass ctx.signal to fetch (or any abortable API) to also cancel the network request itself.
Per-field vs global debounce
// Per field:
asyncValidateOptions: { debounce: 500 }
// Globally (all async validators):
useForm({ validateDebounce: 300, ... })On submit, debounce is bypassed so validation never delays submission.
Handling slow checks
<input {...form.getFieldProps("username")} />
{form.validating.username && (
<p className="hint">Checking availability…</p>
)}
{form.touched.username && form.errors.username && (
<p className="error">{form.errors.username}</p>
)}form.isValidating is also available for a form-wide spinner (it's true while any field is validating).
Error handling
If an async validator throws (non-abort error), the field is treated as valid (the error is logged in development) — a network hiccup should never block the user. For hard requirements, re-check server-side on submit or use a plugin's onSubmit hook (see plugins).