Créer des formulaires React robustes est rarement complexe… mais presque toujours répétitif.
Entre la validation, les composants UI, la gestion des erreurs et l’UX, le boilerplate s’accumule vite.
SnowForm (@snowpact/react-rhf-zod-form) répond à ce problème :
générer automatiquement des formulaires React à partir de schémas Zod,
tout en vous laissant injecter votre propre design system.
TL;DR : Un seul fichier de config définit vos composants UI, puis des formulaires auto-générés depuis Zod. Moins de boilerplate, zéro couplage UI, des formulaires cohérents sur tous vos projets.
Le boilerplate des formulaires React
Créer un formulaire React « proprement », c’est souvent ça :
- Définir un schéma Zod pour la validation
- Configurer
react-hook-formavec le resolver Zod - Créer un composant pour chaque champ (avec label, erreur, etc.)
- Gérer le loading state du submit
- Afficher les erreurs serveur
- Scroll vers la première erreur
- Toast en cas d’erreur
Sur chaque formulaire. Sur chaque projet.
Et si vous changez de design system (Shadcn → MUI), vous devez tout réécrire.
L’approche SnowForm
Notre solution : un seul fichier de config qui définit vos composants UI, puis des formulaires auto-générés depuis Zod.
// Un formulaire complet en 20 lignes
<SnowForm
schema={userSchema}
onSubmit={async (data) => await api.createUser(data)}
onSuccess={() => toast.success('Utilisateur créé !')}
/>
Le package analyse le schéma Zod et génère automatiquement :
- Les champs appropriés (text, email, number, select, etc.)
- Les labels traduits
- Les messages d’erreur
- Le bouton submit avec loading state
Installation
npm install @snowpact/react-rhf-zod-form
Peer Dependencies
| Dépendance | Version | Usage |
|---|---|---|
| react | >= 18.0 | Framework UI |
| react-dom | >= 18.0 | DOM rendering |
| react-hook-form | >= 7.0 | Gestion du state formulaire |
| @hookform/resolvers | >= 3.0 | Connecteur Zod |
| zod | >= 3.24 | Validation de schéma |
Quick Setup
1. Enregistrez vos composants UI
Vous mappez vos composants (Shadcn, MUI, custom…) une seule fois dans un fichier de configuration.
// configs/setupSnowForm.tsx
import {
registerComponents,
registerFormUIStyles,
registerSubmitButton,
setTranslationHook,
setOnErrorBehavior,
} from '@snowpact/react-rhf-zod-form';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
// Vos composants Shadcn/MUI/custom
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
export function setupSnowForm() {
// 1. Enregistrer les composants par type
registerComponents({
text: ({ value, onChange, name, disabled, placeholder }) => (
<Input
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
name={name}
disabled={disabled}
placeholder={placeholder}
/>
),
email: ({ value, onChange, ...props }) => (
<Input type="email" value={value ?? ''} onChange={(e) => onChange(e.target.value)} {...props} />
),
password: ({ value, onChange, ...props }) => (
<Input type="password" value={value ?? ''} onChange={(e) => onChange(e.target.value)} {...props} />
),
select: ({ value, onChange, options }) => (
<Select value={value} onValueChange={onChange} options={options} />
),
checkbox: ({ value, onChange, name }) => (
<Switch id={name} checked={value ?? false} onCheckedChange={onChange} />
),
});
// 2. Styles CSS pour le layout du formulaire
registerFormUIStyles({
form: 'space-y-6 w-full',
formItem: 'grid gap-2',
formLabel: 'text-sm font-medium',
formLabelError: 'text-destructive',
formMessage: 'text-destructive text-sm',
});
// 3. Bouton submit custom
registerSubmitButton(({ loading, disabled, children }) => (
<Button type="submit" disabled={disabled || loading} className="w-full">
{loading ? 'Chargement...' : children}
</Button>
));
// 4. Traductions des labels (optionnel)
setTranslationHook(() => {
const { t } = useTranslation();
return {
t: (key) => {
if (key === 'submit') return 'Envoyer';
return t(`form.fields.${key}`);
},
};
});
// 5. Comportement en cas d'erreur
setOnErrorBehavior((formRef) => {
formRef?.scrollIntoView({ behavior: 'smooth', block: 'start' });
toast.error('Veuillez corriger les erreurs du formulaire');
});
}
2. Initialisez au démarrage
// main.tsx
import { setupSnowForm } from './configs/setupSnowForm';
setupSnowForm();
3. Créez un formulaire
// components/LoginForm.tsx
import { SnowForm } from '@snowpact/react-rhf-zod-form';
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export const LoginForm = () => {
return (
<SnowForm
schema={loginSchema}
onSubmit={async (data) => {
await api.login(data);
}}
onSuccess={() => {
router.push('/dashboard');
}}
/>
);
};
Le formulaire affiche automatiquement :
- Un champ email (type détecté depuis
.email()) - Un champ password (type détecté depuis le nom du champ)
- Labels traduits
- Messages d’erreur Zod
- Bouton submit avec loading
Overrides : Personnaliser les champs
Le système d’overrides permet de modifier le comportement par champ :
<SnowForm
schema={blogpostSchema}
onSubmit={handleSubmit}
overrides={{
// Changer le type de composant
content: {
type: 'rich-text',
description: 'Utilisez Markdown pour le formatage',
},
// Select avec options
category: {
type: 'select',
options: [
{ label: 'Tech', value: 'tech' },
{ label: 'Design', value: 'design' },
],
},
// Champ caché
authorId: {
type: 'hidden',
},
// Transformer les valeurs vides en null
videoUrl: {
type: 'text',
placeholder: 'https://youtube.com/...',
emptyAsNull: true,
},
// Composant custom inline
avatar: {
render: ({ value, onChange }) => (
<ImageUploader value={value} onUpload={onChange} />
),
},
}}
/>
Mode Édition avec fetchDefaultValues
SnowForm gère aussi les formulaires d’édition avec chargement asynchrone des données.
<SnowForm
schema={userSchema}
fetchDefaultValues={async () => {
const { data } = await api.getUser(userId);
return data.user;
}}
onSubmit={async (values) => {
await api.updateUser(userId, values);
}}
onSuccess={() => {
queryClient.invalidateQueries(['user', userId]);
toast.success('Utilisateur mis à jour');
}}
/>
Le formulaire :
- Affiche un skeleton pendant le chargement
- Pré-remplit les champs avec les données
- Gère le submit et l’invalidation du cache
Gestion des Erreurs Serveur
<SnowForm
schema={registerSchema}
onSubmit={async (data) => {
await api.register(data);
}}
onSubmitError={(setManualFormErrors, error) => {
// error.response.data = { errors: [{ field: 'email', message: 'Déjà utilisé' }] }
const fieldErrors = error.response?.data?.errors?.reduce((acc, e) => ({
...acc,
[e.field]: e.message,
}), {});
setManualFormErrors(fieldErrors);
}}
/>
Les erreurs s’affichent directement sous les champs concernés.
Composants Business Personnalisés
Enregistrez vos composants métier une seule fois :
// setup
registerComponents({
// ... composants standards
// Éditeur WYSIWYG
'rich-text': ({ value, onChange }) => (
<TipTapEditor content={value} onUpdate={onChange} />
),
// Sélecteur d'images depuis une bibliothèque
'media-library': ({ value, onChange }) => (
<MediaPicker selectedUrl={value} onSelect={onChange} />
),
// Sélecteur de couleur
'color': ({ value, onChange }) => (
<ColorPicker color={value} onChange={onChange} />
),
});
Puis utilisez-les via overrides :
<SnowForm
schema={pageSchema}
overrides={{
content: { type: 'rich-text' },
heroImage: { type: 'media-library' },
accentColor: { type: 'color' },
}}
/>
Intégration avec les SDK générés
Si vous utilisez un générateur OpenAPI (Orval, openapi-typescript, etc.), les schémas Zod sont déjà générés :
// Le schéma Zod vient directement du SDK généré depuis OpenAPI
import { zodUserCreateBody } from '@/sdk';
<SnowForm
schema={zodUserCreateBody} // Généré automatiquement
onSubmit={async (data) => {
await sdk.createUser(data);
}}
/>
Votre formulaire est toujours synchronisé avec l’API backend.
Tableau des Fonctionnalités
| Fonctionnalité | Description |
|---|---|
| Auto-génération | Formulaire généré depuis le schéma Zod |
| Types de champs | text, email, password, number, textarea, select, checkbox, date, hidden |
| Composants custom | Injectez vos propres composants via registry |
| Overrides | Personnalisez chaque champ individuellement |
| fetchDefaultValues | Mode édition avec données pré-remplies |
| Loading states | Skeleton pendant le fetch, spinner pendant le submit |
| Validation | Zod côté client + erreurs serveur |
| i18n | Labels traduits via hook injectable |
| onError behavior | Scroll + toast personnalisables |
| emptyAsNull | Transforme « » en null pour les champs optionnels |
| TypeScript | Types inférés depuis le schéma Zod |
Mapping Zod → Type de champ
| Schéma Zod | Type détecté |
|---|---|
z.string() |
text |
z.string().email() |
|
z.number() |
number |
z.boolean() |
checkbox |
z.date() |
date |
z.enum([...]) |
select |
| Nom contient « password » | password |
| Nom contient « description » | textarea |
Vous pouvez toujours overrider avec type: 'xxx'.
Comparaison
| Solution | Problème |
|---|---|
| react-hook-form seul | Pas de génération auto, beaucoup de boilerplate |
| Formik | Plus maintenu activement, pas de Zod natif |
| react-jsonschema-form | JSON Schema au lieu de Zod, style imposé |
| AutoForm (shadcn) | Copier-coller, pas de package, couplé à Shadcn |
| SnowForm | Package npm, injectez votre UI, zéro couplage |
Liens
Article rédigé par l’équipe Snowpact. Retrouvez nos projets open-source sur GitHub.
