Skip to main content

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-form avec 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() 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.