React Hook Form avec Claude Code : guide d'implémentation fiable
Construisez des formulaires React Hook Form avec Zod, erreurs accessibles, état d'envoi, tests et prompts Claude Code.
Définir le contrat avant de demander le code
React Hook Form est une bibliothèque légère pour créer des formulaires dans React. Au lieu de stocker chaque frappe dans useState, elle s’appuie sur le comportement natif du navigateur et expose des APIs comme register, handleSubmit et formState. Pour débuter, cela clarifie les questions essentielles : où les valeurs sont collectées, quand la validation s’exécute, et comment éviter un double envoi pendant une requête.
Claude Code peut produire le composant, le schema Zod, la route API, les tests et un premier refactoring dans la même session. C’est pratique, mais un formulaire est souvent relié à un objectif concret : demande de contact, essai produit, questionnaire avant achat, newsletter ou profil utilisateur. Une demande vague comme “crée un formulaire” peut donner une interface correcte visuellement, mais sans erreurs accessibles, validation serveur ou état de soumission robuste.
Ce guide part d’un formulaire de demande pour expliquer useForm, zodResolver, l’affichage d’erreurs, l’état d’envoi, la revalidation côté API, la testabilité et les prompts sûrs pour Claude Code. Pour les patterns React généraux, consultez développement React avec Claude Code. Pour les règles Zod, lisez aussi validation Zod avec Claude Code.
Architecture : placer le schema au centre
React Hook Form pilote le cycle du formulaire. Zod décrit quelles données sont valides. zodResolver, fourni par @hookform/resolvers/zod, relie les deux afin que React Hook Form exécute le schema au moment de valider.
flowchart TD
A["Saisie utilisateur"] --> B["React Hook Form register"]
B --> C["zodResolver valide le schema"]
C --> D{"Données valides"}
D -->|Non| E["Afficher les erreurs"]
D -->|Oui| F["handleSubmit envoie les valeurs"]
F --> G["L'API valide le même schema"]
G --> H["Stocker, notifier ou synchroniser CRM"]
En termes simples, useForm est le contrôleur, le schema Zod est la règle métier, et le resolver est l’adaptateur. Quand vous demandez une modification à Claude Code, nommer ces trois parties limite les réécritures inutiles. Vous pouvez ensuite demander : “mets à jour le schema, le select, l’API et les tests ensemble”.
Pour vérifier les détails, partez des sources officielles : documentation useForm, React Hook Form Resolvers, API Zod, référence React pour <input>, et documentation Claude Code overview et commands.
Schema Zod prêt à copier
Commencez par isoler les règles de validation. L’exemple suivant contient le nom, l’email, la catégorie, le message et le consentement de contact. z.infer dérive le type TypeScript depuis le schema, ce qui évite une interface séparée qui diverge de la validation réelle.
// src/features/inquiry/inquirySchema.ts
import { z } from "zod";
export const inquirySchema = z.object({
name: z
.string()
.trim()
.min(1, "Saisissez votre nom")
.max(80, "Le nom doit contenir au maximum 80 caractères"),
email: z
.string()
.trim()
.email("Saisissez une adresse email valide"),
category: z.enum(["consulting", "support", "billing"], {
error: "Choisissez une catégorie",
}),
message: z
.string()
.trim()
.min(10, "Saisissez au moins 10 caractères")
.max(1000, "Le message doit contenir au maximum 1000 caractères"),
agreeToContact: z.boolean().refine((value) => value, {
message: "Le consentement de contact est obligatoire",
}),
});
export type InquiryFormValues = z.infer<typeof inquirySchema>;
La catégorie est un enum parce que la valeur envoyée doit rester prévisible. En production, elle peut décider d’un routage vers ventes, support, facturation ou CRM. Dans le prompt Claude Code, indiquez toujours le libellé visible et la valeur transmise, par exemple “libellé : Support technique, valeur : support”. Le texte peut être localisé, la valeur doit rester stable.
Gérer valeurs, erreurs et envoi avec useForm
Le composant suivant connecte le schema à React Hook Form. mode: "onBlur" valide quand la personne quitte le champ. Pour un formulaire de contact, c’est souvent plus confortable que d’afficher une erreur à chaque frappe. Au moment de soumettre, handleSubmit valide une dernière fois avant d’appeler onSubmit.
// src/features/inquiry/InquiryForm.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { inquirySchema, type InquiryFormValues } from "./inquirySchema";
async function sendInquiry(values: InquiryFormValues) {
const response = await fetch("/api/inquiry", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
if (!response.ok) {
throw new Error("Failed to send inquiry");
}
}
export function InquiryForm() {
const {
register,
handleSubmit,
reset,
setError,
formState: { errors, isSubmitting },
} = useForm<InquiryFormValues>({
resolver: zodResolver(inquirySchema),
mode: "onBlur",
defaultValues: {
name: "",
email: "",
message: "",
agreeToContact: false,
},
});
const onSubmit = async (values: InquiryFormValues) => {
try {
await sendInquiry(values);
reset();
} catch {
setError("root", {
type: "server",
message: "Impossible d'envoyer le formulaire. Réessayez dans un instant.",
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<div>
<label htmlFor="name">Nom</label>
<input
id="name"
autoComplete="name"
aria-invalid={errors.name ? "true" : "false"}
aria-describedby={errors.name ? "name-error" : undefined}
{...register("name")}
/>
{errors.name && (
<p id="name-error" role="alert">
{errors.name.message}
</p>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
autoComplete="email"
aria-invalid={errors.email ? "true" : "false"}
aria-describedby={errors.email ? "email-error" : undefined}
{...register("email")}
/>
{errors.email && (
<p id="email-error" role="alert">
{errors.email.message}
</p>
)}
</div>
<div>
<label htmlFor="category">Sujet</label>
<select
id="category"
aria-invalid={errors.category ? "true" : "false"}
aria-describedby={errors.category ? "category-error" : undefined}
{...register("category")}
>
<option value="">Choisir</option>
<option value="consulting">Conseil d'implémentation</option>
<option value="support">Support technique</option>
<option value="billing">Facturation ou contrat</option>
</select>
{errors.category && (
<p id="category-error" role="alert">
{errors.category.message}
</p>
)}
</div>
<div>
<label htmlFor="message">Message</label>
<textarea
id="message"
rows={6}
aria-invalid={errors.message ? "true" : "false"}
aria-describedby={errors.message ? "message-error" : undefined}
{...register("message")}
/>
{errors.message && (
<p id="message-error" role="alert">
{errors.message.message}
</p>
)}
</div>
<label>
<input type="checkbox" {...register("agreeToContact")} />
J'accepte d'être contacté au sujet de cette demande
</label>
{errors.agreeToContact && (
<p role="alert">{errors.agreeToContact.message}</p>
)}
{errors.root && <p role="alert">{errors.root.message}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Envoi..." : "Envoyer la demande"}
</button>
</form>
);
}
Le point clé est le lien entre champ et message. Chaque champ indique aria-invalid, chaque message utilise role="alert", et aria-describedby relie le champ à son erreur. Cela améliore l’accessibilité et rend les tests plus fiables. Voir aussi accessibilité avec Claude Code.
Revalider côté serveur
La validation côté client améliore l’expérience, mais ne protège pas l’API. Une requête peut être envoyée sans passer par le formulaire. Réutilisez donc le même schema côté serveur avant toute sauvegarde, notification ou synchronisation CRM.
// app/api/inquiry/route.ts
import { NextResponse } from "next/server";
import { inquirySchema } from "@/features/inquiry/inquirySchema";
export async function POST(request: Request) {
const payload = await request.json().catch(() => null);
const parsed = inquirySchema.safeParse(payload);
if (!parsed.success) {
return NextResponse.json(
{
error: "Invalid inquiry",
fields: parsed.error.flatten().fieldErrors,
},
{ status: 400 },
);
}
// TODO: Enregistrer, envoyer un email ou synchroniser le CRM.
return NextResponse.json({ ok: true });
}
Dans le prompt, demandez explicitement : “réutilise inquirySchema, retourne 400 avec les erreurs de champs, et laisse email ou CRM en TODO”. Les secrets, les retries et la déduplication méritent une tâche séparée.
Tester les comportements importants
Un formulaire peut casser sans que l’écran paraisse cassé. Testez au minimum l’envoi vide, l’envoi valide, l’échec serveur et la désactivation du bouton. Avec Vitest et React Testing Library, vous pouvez vérifier les erreurs et l’appel à fetch.
// src/features/inquiry/InquiryForm.test.tsx
import { afterEach, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { InquiryForm } from "./InquiryForm";
afterEach(() => {
vi.unstubAllGlobals();
});
test("affiche les erreurs quand le formulaire est vide", async () => {
render(<InquiryForm />);
await userEvent.click(screen.getByRole("button", { name: "Envoyer la demande" }));
expect(await screen.findAllByRole("alert")).toHaveLength(5);
});
test("envoie les valeurs valides à l'API", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response("{}", { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
render(<InquiryForm />);
await userEvent.type(screen.getByLabelText("Nom"), "Masa");
await userEvent.type(screen.getByLabelText("Email"), "masa@example.com");
await userEvent.selectOptions(screen.getByLabelText("Sujet"), "consulting");
await userEvent.type(
screen.getByLabelText("Message"),
"Je veux implémenter React Hook Form de manière fiable.",
);
await userEvent.click(
screen.getByLabelText("J'accepte d'être contacté au sujet de cette demande"),
);
await userEvent.click(screen.getByRole("button", { name: "Envoyer la demande" }));
expect(fetchMock).toHaveBeenCalledWith(
"/api/inquiry",
expect.objectContaining({ method: "POST" }),
);
});
Vous pouvez demander à Claude Code d’écrire d’abord les tests qui échouent, puis l’implémentation. Pour les parcours navigateur complets, combinez avec tests Playwright avec Claude Code. Pour la mesure business, déclenchez l’événement après acceptation serveur, pas au simple clic ; voir analytics avec Claude Code.
Prompt sûr pour Claude Code
Un bon prompt précise le périmètre, les contraintes, les commandes de vérification et ce qui est hors scope.
Implémente un formulaire de demande avec React Hook Form et Zod.
Périmètre:
- Modifier uniquement src/features/inquiry et app/api/inquiry.
- Utiliser useForm, zodResolver et les types TypeScript dérivés du schema.
- Champs: name, email, category, message, agreeToContact.
- Afficher les erreurs avec role="alert" et aria-describedby.
- Désactiver le bouton quand isSubmitting vaut true.
- Revalider le même schema Zod dans la route API.
- Ajouter des tests Vitest + Testing Library.
Vérification:
- npm test -- InquiryForm
- npm run typecheck
Ne pas faire:
- Ne pas ajouter de nouvelle bibliothèque UI.
- Ne pas renommer les valeurs category existantes.
- Ne pas implémenter email réel, CRM ou secrets dans cette tâche.
Pour une modification, soyez encore plus précis : “ajoute le libellé Demande de formation avec la valeur training, puis mets à jour enum, select, API, tests et mapping analytics”. Claude Code peut chercher les fichiers, mais le contrat doit être défini par vous.
Cas d’usage
| Cas | Structure adaptée | Point de vigilance |
|---|---|---|
| Formulaire de contact | Zod + React Hook Form + revalidation API | Mesurer les demandes réussies, pas les clics |
| Profil utilisateur | Charger les données dans defaultValues | Appeler reset(savedValues) après sauvegarde |
| Questionnaire avant achat | Combiner select, radio, checkbox | Aligner valeurs avec produit ou CRM |
| Recherche admin | Validation légère et synchronisation URL query | Éviter une requête API à chaque frappe |
La règle commune est de séparer libellé visible et valeur envoyée. Les libellés se traduisent et changent. Les valeurs envoyées doivent rester stables, car rapports, automatisations et backend en dépendent.
Pièges fréquents
Le premier piège est de valider uniquement dans le navigateur. Importez le schema partagé dans la route API et appelez safeParse avant de traiter le payload.
Le deuxième est de perdre isSubmitting trop tôt. Si onSubmit n’attend pas le travail asynchrone, le bouton peut se réactiver pendant l’envoi.
Le troisième est d’éloigner les erreurs des champs. Une bannière globale peut aider, mais chaque champ doit avoir son message proche.
Le quatrième est de laisser Claude Code inventer un nouveau design system. Si TextField, Select, Button ou toast existent déjà, demandez explicitement de les réutiliser.
Le cinquième est d’oublier l’après-envoi : message de succès, page de remerciement, email, événement analytics et CRM doivent être planifiés.
CTA de monétisation
La qualité d’un formulaire se mesure au tunnel qu’il soutient. Avant de refactorer, choisissez l’événement business : inscription PDF, lead produit, achat de template ou demande de conseil.
Pour apprendre seul, parcourez les ressources de la page produits. Pour appliquer Claude Code aux formulaires d’une équipe, démarrez par formation et conseil. Un formulaire est petit, mais il est souvent la porte entre contenu utile et revenus.
Résultat testé
Masa a testé cette structure sur un petit flux de demande. Le plus utile a été de centraliser le schema, car cela a évité d’ajouter une option dans le select sans mettre à jour les valeurs autorisées côté API. Les tests d’envoi vide et d’envoi valide ont aussi été précieux. Après des modifications demandées à Claude Code, ils ont détecté rapidement des messages d’erreur oubliés et un appel fetch cassé. En pratique, traiter le formulaire comme un contrat d’entrée est plus maintenable que le traiter comme une simple UI.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.