En 2025, les formulaires web continueront d’être l’un des principaux points d’interaction entre les utilisateurs et les applications. Alors que React Hook Form évolue considérablement, explorons les meilleures pratiques pour créer des formulaires complexes qui non seulement fonctionnent bien, mais offrent également une expérience utilisateur exceptionnelle.
Fondamentaux de l'UX dans les formulaires
Principes essentiels
- Commentaires immédiats : validation en temps réel
- Prévention des erreurs : guidez l'utilisateur avant qu'une erreur ne se produise
- Récupération d'erreur : messages clairs et actions correctives
- Progression naturelle : flux de remplissage logique
- Accessibilité : répondre à différents besoins
Implémentation avec React Hook Form
Configuration initiale avec TypeScript
// src/types/form.ts interface FormData { personalInfo: { name: string; email: string; phone: string; }; address: { street: string; city: string; state: string; zipCode: string; }; preferences: { notifications: boolean; theme: 'light' | 'dark'; language: string; }; } // src/components/ComplexForm.tsx import { useForm, useFieldArray } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; const formSchema = z.object({ personalInfo: z.object({ name: z.string().min(2, 'Nome muito curto'), email: z.string().email('Email inválido'), phone: z.string().regex(/^\d{10,11}$/, 'Telefone inválido'), }), address: z.object({ street: z.string().min(5, 'Endereço muito curto'), city: z.string().min(2, 'Cidade inválida'), state: z.string().length(2, 'Use a sigla do estado'), zipCode: z.string().regex(/^\d{8}$/, 'CEP inválido'), }), preferences: z.object({ notifications: z.boolean(), theme: z.enum(['light', 'dark']), language: z.string(), }), });
Composant de formulaire de base
// src/components/SmartForm/index.tsx import * as React from 'react'; import { useForm } from 'react-hook-form'; import { cn } from '@/lib/utils'; interface SmartFormProps<T> { defaultValues: T; onSubmit: (data: T) => Promise<void>; children: React.ReactNode; className?: string; } export function SmartForm<T>({ defaultValues, onSubmit, children, className, }: SmartFormProps<T>) { const form = useForm<T>({ defaultValues, mode: 'onChange', }); const [isSubmitting, setIsSubmitting] = React.useState(false); const [submitError, setSubmitError] = React.useState<string | null>(null); const handleSubmit = async (data: T) => { try { setIsSubmitting(true); setSubmitError(null); await onSubmit(data); } catch (error) { setSubmitError(error.message); } finally { setIsSubmitting(false); } }; return ( <form onSubmit={form.handleSubmit(handleSubmit)} className={cn('space-y-8', className)} > <FormProvider {...form}> {children} </FormProvider> {submitError && ( <div className="text-red-500 text-sm mt-2"> {submitError} </div> )} <button type="submit" disabled={isSubmitting} className={cn( 'px-4 py-2 bg-primary text-white rounded-md', 'disabled:opacity-50 disabled:cursor-not-allowed' )} > {isSubmitting ? 'Enviando...' : 'Enviar'} </button> </form> ); }
Champs intelligents avec commentaires
// src/components/SmartField/index.tsx import * as React from 'react'; import { useFormContext } from 'react-hook-form'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Info, AlertCircle } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; interface SmartFieldProps { name: string; label: string; help?: string; type?: string; placeholder?: string; validation?: Record<string, unknown>; } export function SmartField({ name, label, help, type = 'text', placeholder, validation, }: SmartFieldProps) { const { register, formState: { errors }, watch, } = useFormContext(); const value = watch(name); const error = errors[name]; return ( <div className="space-y-2"> <div className="flex items-center space-x-2"> <Label htmlFor={name}>{label}</Label> {help && ( <Tooltip> <TooltipTrigger asChild> <Info className="h-4 w-4 text-muted-foreground" /> </TooltipTrigger> <TooltipContent>{help}</TooltipContent> </Tooltip> )} </div> <div className="relative"> <Input id={name} type={type} placeholder={placeholder} {...register(name, validation)} className={cn( 'w-full', error && 'border-red-500 focus:border-red-500' )} aria-describedby={`${name}-error`} /> {error && ( <div className="absolute right-2 top-1/2 -translate-y-1/2" id={`${name}-error`} > <Tooltip> <TooltipTrigger asChild> <AlertCircle className="h-4 w-4 text-red-500" /> </TooltipTrigger> <TooltipContent> {error.message} </TooltipContent> </Tooltip> </div> )} </div> {value && !error && ( <p className="text-sm text-green-500"> ✓ Campo preenchido corretamente </p> )} </div> ); }
Formulaire en plusieurs étapes
// src/components/MultiStepForm/index.tsx import * as React from 'react'; import { useForm, FormProvider } from 'react-hook-form'; import { motion, AnimatePresence } from 'framer-motion'; interface Step { id: string; title: string; component: React.ComponentType; validation: Record<string, unknown>; } interface MultiStepFormProps { steps: Step[]; onComplete: (data: any) => Promise<void>; } export function MultiStepForm({ steps, onComplete, }: MultiStepFormProps) { const [currentStep, setCurrentStep] = React.useState(0); const [formData, setFormData] = React.useState({}); const form = useForm({ mode: 'onChange', }); const { handleSubmit, trigger } = form; const nextStep = async () => { const fields = Object.keys(steps[currentStep].validation); const isValid = await trigger(fields); if (isValid) { const stepData = form.getValues(fields); setFormData((prev) => ({ ...prev, ...stepData })); setCurrentStep((prev) => prev + 1); } }; const previousStep = () => { setCurrentStep((prev) => prev - 1); }; const onSubmit = async (data: any) => { const finalData = { ...formData, ...data }; await onComplete(finalData); }; const CurrentStepComponent = steps[currentStep].component; return ( <FormProvider {...form}> <div className="space-y-8"> {/* Progress Bar */} <div className="relative pt-1"> <div className="flex mb-2 items-center justify-between"> {steps.map((step, index) => ( <div key={step.id} className={cn( 'text-xs font-semibold inline-block py-1', currentStep >= index ? 'text-primary' : 'text-gray-400' )} > {step.title} </div> ))} </div> <div className="overflow-hidden h-2 mb-4 rounded bg-gray-200"> <motion.div className="h-full bg-primary" initial= animate= transition= /> </div> </div> {/* Form Steps */} <AnimatePresence mode="wait"> <motion.div key={currentStep} initial= animate= exit= > <CurrentStepComponent /> </motion.div> </AnimatePresence> {/* Navigation */} <div className="flex justify-between pt-4"> <button type="button" onClick={previousStep} disabled={currentStep === 0} className={cn( 'px-4 py-2 bg-gray-200 rounded-md', 'disabled:opacity-50 disabled:cursor-not-allowed' )} > Anterior </button> {currentStep === steps.length - 1 ? ( <button type="submit" onClick={handleSubmit(onSubmit)} className="px-4 py-2 bg-primary text-white rounded-md" > Concluir </button> ) : ( <button type="button" onClick={nextStep} className="px-4 py-2 bg-primary text-white rounded-md" > Próximo </button> )} </div> </div> </FormProvider> ); }
Validation asynchrone
// src/hooks/useAsyncValidation.ts import * as React from 'react'; import { useFormContext } from 'react-hook-form'; import { debounce } from 'lodash'; interface AsyncValidationOptions { validateFn: (value: any) => Promise<boolean>; debounceMs?: number; } export function useAsyncValidation( fieldName: string, options: AsyncValidationOptions ) { const { validateFn, debounceMs = 500 } = options; const { watch, setError, clearErrors } = useFormContext(); const value = watch(fieldName); const debouncedValidation = React.useMemo( () => debounce(async (value: any) => { try { const isValid = await validateFn(value); if (!isValid) { setError(fieldName, { type: 'async', message: 'Valor inválido', }); } else { clearErrors(fieldName); } } catch (error) { setError(fieldName, { type: 'async', message: error.message, }); } }, debounceMs), [fieldName, validateFn, debounceMs] ); React.useEffect(() => { if (value) { debouncedValidation(value); } return () => { debouncedValidation.cancel(); }; }, [value, debouncedValidation]); }
Modèles UX avancés
1. Rétroaction visuelle progressive
// src/components/ProgressiveFeedback/index.tsx import * as React from 'react'; import { useFormContext } from 'react-hook-form'; import { motion } from 'framer-motion'; export function ProgressiveFeedback() { const { formState } = useFormContext(); const { isValid, errors, dirtyFields } = formState; const totalFields = Object.keys(dirtyFields).length; const completedFields = totalFields - Object.keys(errors).length; const progress = totalFields ? (completedFields / totalFields) * 100 : 0; return ( <div className="space-y-2"> <div className="flex justify-between text-sm"> <span>Progresso do formulário</span> <span>{Math.round(progress)}%</span> </div> <div className="h-2 bg-gray-200 rounded-full overflow-hidden"> <motion.div className="h-full bg-primary" initial= animate= transition= /> </div> {isValid && ( <motion.p initial= animate= className="text-green-500 text-sm" > ✓ Todos os campos estão preenchidos corretamente! </motion.p> )} </div> ); }
2. Sauvegarde automatique
// src/hooks/useAutoSave.ts import * as React from 'react'; import { useFormContext } from 'react-hook-form'; import { debounce } from 'lodash'; interface AutoSaveOptions { onSave: (data: any) => Promise<void>; debounceMs?: number; enabled?: boolean; } export function useAutoSave(options: AutoSaveOptions) { const { onSave, debounceMs = 1000, enabled = true, } = options; const { watch, formState } = useFormContext(); const { isDirty } = formState; const formData = watch(); const debouncedSave = React.useMemo( () => debounce(async (data: any) => { try { await onSave(data); } catch (error) { console.error('Erro ao salvar:', error); } }, debounceMs), [onSave, debounceMs] ); React.useEffect(() => { if (enabled && isDirty) { debouncedSave(formData); } return () => { debouncedSave.cancel(); }; }, [formData, enabled, isDirty, debouncedSave]); }
3. Navigation au clavier
// src/hooks/useKeyboardNavigation.ts import * as React from 'react'; export function useKeyboardNavigation() { const handleKeyPress = React.useCallback((event: KeyboardEvent) => { const activeElement = document.activeElement; if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); const form = activeElement?.closest('form'); if (!form) return; const inputs = Array.from( form.querySelectorAll( 'input:not([type="hidden"]), select, textarea' ) ); const currentIndex = inputs.indexOf(activeElement as Element); const nextInput = inputs[currentIndex + 1]; if (nextInput) { (nextInput as HTMLElement).focus(); } } }, []); React.useEffect(() => { document.addEventListener('keydown', handleKeyPress); return () => { document.removeEventListener('keydown', handleKeyPress); }; }, [handleKeyPress]); }
Meilleures pratiques de mise en œuvre
1. Organisation des Validations
// src/validations/form-schemas.ts import { z } from 'zod'; export const addressSchema = z.object({ street: z.string().min(5, 'Endereço muito curto'), number: z.string().min(1, 'Número é obrigatório'), complement: z.string().optional(), neighborhood: z.string().min(2, 'Bairro muito curto'), city: z.string().min(2, 'Cidade muito curta'), state: z.string().length(2, 'Use a sigla do estado'), zipCode: z.string().regex(/^\d{8}$/, 'CEP inválido'), }); export const contactSchema = z.object({ email: z.string().email('Email inválido'), phone: z.string().regex(/^\d{10,11}$/, 'Telefone inválido'), preferredContact: z.enum(['email', 'phone']), }); export const completeFormSchema = z.object({ personal: personalSchema, address: addressSchema, contact: contactSchema, });
2. Gestion des erreurs
// src/components/ErrorBoundary/index.tsx import * as React from 'react'; import { AlertTriangle } from 'lucide-react'; interface ErrorBoundaryProps { children: React.ReactNode; fallback?: React.ReactNode; } interface ErrorBoundaryState { hasError: boolean; error?: Error; } export class ErrorBoundary extends React.Component< ErrorBoundaryProps, ErrorBoundaryState > { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } render() { if (this.state.hasError) { return ( this.props.fallback || ( <div className="p-4 border border-red-200 rounded-md bg-red-50"> <div className="flex items-center space-x-2 text-red-600"> <AlertTriangle className="h-5 w-5" /> <h3 className="font-medium"> Algo deu errado </h3> </div> <p className="mt-2 text-sm text-red-500"> Por favor, tente novamente mais tarde ou contate o suporte. </p> </div> ) ); } return this.props.children; } }
Liste de contrôle de mise en œuvre
Avant le développement
- Mappez tous les champs obligatoires
- Définir les règles de validation
- Planifier le débit de remplissage
- Identifier les champs interdépendants
Pendant le développement
- Implémenter des validations synchrones
- Ajouter des validations asynchrones
- Configurer le retour visuel
- Testez différents scénarios
Post-développement
- Vérifier accessibilité
- Test sur différents appareils
- Valider les performances
- Documenter les comportements spéciaux
Conclusion
La création de formulaires complexes avec React Hook Form nécessite un équilibre entre fonctionnalité et expérience utilisateur. Les meilleures pratiques incluent :
- Commentaires immédiats : validation en temps réel et messages clairs
- Accessibilité : prise en charge de la navigation au clavier et des lecteurs d'écran
- Performance : validation optimisée et rendu efficace
- Convivialité : interface intuitive et prévention des erreurs
- Maintenabilité : code organisé et bien documenté
Prochaines étapes
- Mettez en œuvre les exemples dans votre projet
- Adaptez les valeurs par défaut à vos besoins
- Recueillir les commentaires des utilisateurs
- Itérer et améliorer continuellement
Développez-vous des formulaires complexes ? Partagez vos expériences et vos questions dans les commentaires ci-dessous !
A lire aussi
- Systèmes de conception modernes : création de bibliothèques de composants évolutives avec React et TypeScript
- Bonnes pratiques d'internationalisation (i18n) dans React et Next.js en 2025
- Accessibilité numérique UX
- Accessibilité dans les applications mobiles
- Conception centrée sur l'utilisateur : comment choisir d'évoluer
- Conception d'interaction : comment choisir avec la liste de contrôle
