Validation
Learn about Zod integration, async validation, and error handling patterns.
Zod Integration
Define a Zod schema and pass it to TanStack Form through the dynamic validation channel.
import {
Form,
InputField,
} from "@tilt-legal/cubitt-components/form";
import { revalidateLogic, useForm } from "@tanstack/react-form";
import { z } from "zod";const schema = z.object({
email: z.email("Please enter a valid email"),
age: z.number().min(18, "You must be at least 18 years old"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
const form = useForm({
defaultValues: {
email: "",
age: 18,
password: "",
},
validationLogic: revalidateLogic({
mode: "submit",
modeAfterSubmission: "change",
}),
validators: {
onDynamic: schema,
},
});Cross-Field Validation
Use Zod refinements for validation that depends on multiple fields.
const schema = z
.object({
password: z.string().min(8),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});Async Validation
Use async validators for checks such as username or email availability.
const checkUsernameAvailability = async (username: string) => {
await new Promise((resolve) => setTimeout(resolve, 500));
return !["admin", "user", "test"].includes(username.toLowerCase());
};
const schema = z.object({
username: z.string().min(3, "Username must be at least 3 characters"),
email: z.email("Invalid email address"),
});export function AsyncValidationExample() {
const form = useForm({
defaultValues: {
username: "",
email: "",
},
validationLogic: revalidateLogic({
mode: "submit",
modeAfterSubmission: "change",
}),
validators: {
onDynamic: schema,
onDynamicAsyncDebounceMs: 500,
onDynamicAsync: async ({ value }) => {
if (value.username.length < 3) {
return;
}
const available = await checkUsernameAvailability(value.username);
if (!available) {
return {
fields: {
username: "Username is already taken",
},
};
}
},
},
});
return (
<Form form={form} className="max-w-sm">
<InputField
name="username"
label="Username"
placeholder="Choose a unique username"
/>
<InputField
name="email"
label="Email"
type="email"
placeholder="your@email.com"
/>
</Form>
);
}Error Handling
Field components read TanStack field meta and render errors through the shared form message chrome.
<InputField name="username" label="Username" />For advanced field-level control, pass a field instance directly.
<form.Field name="username">
{(field) => <InputField field={field} label="Username" />}
</form.Field>Server-Side Errors
Server validation errors can be mapped back onto field meta with the form helpers.
import { setFieldErrors } from "@tilt-legal/cubitt-components/form";
const form = useForm({
onSubmit: async ({ value, formApi }) => {
try {
await createAccount(value);
} catch (error) {
if (error.status === 422) {
for (const [fieldName, message] of Object.entries(error.errors)) {
setFieldErrors(formApi, fieldName, [String(message)]);
}
}
}
},
});Helper Utilities
import {
extractErrorMessages,
hasErrors,
setFieldErrors,
setFieldMeta,
setFieldValue,
validateField,
validateForm,
} from "@tilt-legal/cubitt-components/form";| Utility | Description |
|---|---|
extractErrorMessages | Converts TanStack field errors to strings |
hasErrors | Checks whether a field has visible errors |
setFieldErrors | Sets custom errors on a field |
setFieldMeta | Updates field meta with an updater |
setFieldValue | Sets a field value |
validateField | Runs validation for one field |
validateForm | Runs validation for the full form |
These utilities are primarily used internally by the FormBuilder components. For most use cases, the standard TanStack Form API is sufficient.