Submitting forms
Every form and block takes one onSubmit function. It receives the validated values and returns what happened, and the form does the rest: pending state, errors beside their fields, and focus on the first problem. It works the same with a server action, a fetch to your API, or any other client.
With a server action
Validate on the server with validateForm and the block’s own schema. When it fails, return it as it is: its errors are already on the paths the form uses.
Each block keeps its schema in a file of its own, such as sign-up-schema.ts, without "use client". Import it from there: a schema imported from the form’s file reaches the server as a client reference, and validating with it fails.
What onSubmit returns
Nothing, or { ok: true }- Success. onSuccess runs with the result, so you can redirect, reset, or show a message.
{ ok: true, data }- Success with something to hand back, such as the new project's id for a redirect.
{ ok: false, fieldErrors }- Errors shown beside their fields, keyed by path: "email", "shipping.postalCode", "invites.2.email". Focus moves to the first one.
{ ok: false, formError }- One message above the form, for problems that belong to no field: a refused sign-in, a rate limit, an expired link.
A thrown error- A dropped connection or a crash: the form says something went wrong and onError receives the error for your reporting.
A field error for a path that isn’t on screen, such as a field on another step of a wizard, is shown with the form error rather than dropped.
Your own forms
The blocks are built from the same parts you can use directly: useZodForm, Form, the field components, and FormSubmit. Give the schema its own file, as the blocks do, so your action can validate with it.
On the server
- Validate every submit on the server with the same schema. Client checks, live availability checks included, are a convenience that anyone can skip.
- Answer a wrong email or password with one formError that doesn't say which was wrong, and a password reset request with success whether or not the email has an account. Anything else tells strangers who your users are.
- Put rate limits and lockouts in formError, with when to try again.
- Check the contact form's honeypot (website) on the server. A filled one is a bot: answer with success and drop it.
- Collect card details with your payment provider's hosted fields in the checkout's payment slot, never with your own inputs.
- Send dates from the date field with toDateString(), not toISOString(), so the day doesn't move across time zones.