Integrazione con React Hook Form
Perche’ Usare Zod con React Hook Form
React Hook Form e’ una delle librerie piu’ popolari per la gestione dei form in React. Integrandola con Zod ottieni:
- Validazione type-safe: Lo schema Zod definisce sia la validazione che il tipo TypeScript
- Messaggi di errore personalizzati: Gestiti direttamente nello schema Zod
- Un’unica fonte di verita’: Non devi duplicare le regole di validazione
Installazione
npm install react-hook-form @hookform/resolvers zod
Setup Base
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// 1. Definisci lo schema Zod
const LoginSchema = z.object({
email: z.string()
.min(1, "L'email e' obbligatoria")
.email("Formato email non valido"),
password: z.string()
.min(8, "La password deve avere almeno 8 caratteri"),
});
// 2. Inferisci il tipo
type LoginForm = z.infer<typeof LoginSchema>;
// 3. Usa nel componente
function LoginPage() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginForm>({
resolver: zodResolver(LoginSchema),
});
const onSubmit = async (dati: LoginForm) => {
// dati e' gia' validato e tipizzato
console.log(dati.email, dati.password);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input {...register("email")} placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<input {...register("password")} type="password" placeholder="Password" />
{errors.password && <span>{errors.password.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
Accedi
</button>
</form>
);
}
Esempio Completo: Form di Registrazione
const RegistrazioneSchema = z.object({
nome: z.string()
.min(2, "Il nome deve avere almeno 2 caratteri")
.max(50, "Il nome non puo' superare i 50 caratteri"),
cognome: z.string()
.min(2, "Il cognome deve avere almeno 2 caratteri"),
email: z.string()
.min(1, "L'email e' obbligatoria")
.email("Email non valida"),
eta: z.coerce
.number({ invalid_type_error: "L'eta' deve essere un numero" })
.int("L'eta' deve essere un numero intero")
.min(13, "Devi avere almeno 13 anni")
.max(120, "Eta' non valida"),
password: z.string()
.min(8, "Minimo 8 caratteri")
.regex(/[A-Z]/, "Deve contenere almeno una maiuscola")
.regex(/[0-9]/, "Deve contenere almeno un numero")
.regex(/[^A-Za-z0-9]/, "Deve contenere almeno un carattere speciale"),
confermaPassword: z.string(),
accettaTermini: z.literal(true, {
errorMap: () => ({ message: "Devi accettare i termini e le condizioni" }),
}),
}).refine((dati) => dati.password === dati.confermaPassword, {
message: "Le password non corrispondono",
path: ["confermaPassword"],
});
type RegistrazioneForm = z.infer<typeof RegistrazioneSchema>;
function RegistrazionePage() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<RegistrazioneForm>({
resolver: zodResolver(RegistrazioneSchema),
defaultValues: {
nome: "",
cognome: "",
email: "",
password: "",
confermaPassword: "",
},
});
const onSubmit = async (dati: RegistrazioneForm) => {
const response = await fetch("/api/registrazione", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(dati),
});
// gestisci la risposta...
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Nome</label>
<input {...register("nome")} />
{errors.nome && <p className="errore">{errors.nome.message}</p>}
</div>
<div>
<label>Cognome</label>
<input {...register("cognome")} />
{errors.cognome && <p className="errore">{errors.cognome.message}</p>}
</div>
<div>
<label>Email</label>
<input {...register("email")} type="email" />
{errors.email && <p className="errore">{errors.email.message}</p>}
</div>
<div>
<label>Eta'</label>
<input {...register("eta")} type="number" />
{errors.eta && <p className="errore">{errors.eta.message}</p>}
</div>
<div>
<label>Password</label>
<input {...register("password")} type="password" />
{errors.password && <p className="errore">{errors.password.message}</p>}
</div>
<div>
<label>Conferma Password</label>
<input {...register("confermaPassword")} type="password" />
{errors.confermaPassword && (
<p className="errore">{errors.confermaPassword.message}</p>
)}
</div>
<div>
<label>
<input {...register("accettaTermini")} type="checkbox" />
Accetto i termini e le condizioni
</label>
{errors.accettaTermini && (
<p className="errore">{errors.accettaTermini.message}</p>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Registrazione..." : "Registrati"}
</button>
</form>
);
}
Valori di Default e Trasformazioni
Puoi usare defaultValues con lo schema:
const ProfiloSchema = z.object({
bio: z.string().max(500).default(""),
sito: z.string().url().or(z.literal("")).default(""),
newsletter: z.boolean().default(false),
});
type ProfiloForm = z.infer<typeof ProfiloSchema>;
function ProfiloPage() {
const { register, handleSubmit } = useForm<ProfiloForm>({
resolver: zodResolver(ProfiloSchema),
defaultValues: {
bio: "",
sito: "",
newsletter: false,
},
});
// ...
}
Validazione in Modalita’ onChange
Per validare mentre l’utente digita:
const { register, handleSubmit, formState: { errors } } = useForm<LoginForm>({
resolver: zodResolver(LoginSchema),
mode: "onChange", // valida ad ogni cambiamento
// oppure: mode: "onBlur" -- valida quando il campo perde il focus
});
Errori Lato Server
Puoi combinare la validazione Zod con errori dal server:
const { setError, handleSubmit } = useForm<LoginForm>({
resolver: zodResolver(LoginSchema),
});
const onSubmit = async (dati: LoginForm) => {
const risposta = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(dati),
});
if (!risposta.ok) {
const errore = await risposta.json();
// Imposta errori dal server
setError("email", { message: errore.messaggio });
// Oppure errore generico del form
setError("root", { message: "Credenziali non valide" });
}
};