Client-Side Validation Example
v4A signup form with onChange client-side Zod validation. The same schema used on the server validates fields instantly in the browser — no duplicate validation logic needed.
How It Works
1.
withZod(schema, action) wraps the server action and attaches the Zod schema via __schema.
2.
useActionForm auto-detects the attached schema and subscribes to field changes with form.watch().
3.
In onChange mode, every keystroke runs a partial safeParse and sets/clears field errors instantly.
4.
On submit, client validation runs first. Only if all fields pass does the server action execute, saving a round-trip for obvious errors.
5.
Server-side errors (like "username taken") still come through and are merged into RHF's error state seamlessly.
Source Code
'use client'
import { useActionForm } from 'hookform-action'
import { signupAction } from './actions'
import { signupSchema } from './schema'
export function SignupForm() {
// Same Zod schema used on both client and server
const { register, handleSubmit, formState: { errors, isPending } } =
useActionForm(signupAction, {
defaultValues: { username: '', email: '', password: '' },
schema: signupSchema, // enables client-side validation
validationMode: 'onChange', // 'onBlur' | 'onSubmit' also available
})
return (
<form onSubmit={handleSubmit()}>
<input {...register('username')} />
{errors.username && <span>{errors.username.message}</span>}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('password')} type="password" />
{errors.password && <span>{errors.password.message}</span>}
<button disabled={isPending}>Create Account</button>
</form>
)
}