Nel 2025 i moduli web continueranno a essere uno dei principali punti di interazione tra utenti e applicazioni. Con l'evoluzione significativa di React Hook Form, esploriamo le migliori pratiche per creare moduli complessi che non solo funzionano bene, ma forniscono anche un'esperienza utente eccezionale.
Fondamenti di UX nei moduli
Principi essenziali
- Feedback immediato: convalida in tempo reale
- Prevenzione degli errori: guida l'utente prima che si verifichi un errore
- Recupero errori: messaggi chiari e azioni correttive
- Progressione naturale: flusso di riempimento logico
- Accessibilità: supporta esigenze diverse
Implementazione con React Hook Form
Configurazione iniziale con 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(), }), });
Componente del modulo 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> ); }
Campi intelligenti con feedback
// 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> ); }
Modulo a più passaggi
// 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> ); }
Convalida asincrona
// 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]); }
Modelli UX avanzati
1. Feedback visivo progressivo
// 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. Salvataggio automatico
// 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. Navigazione tramite tastiera
// 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]); }
Migliori pratiche di implementazione
1. Organizzazione delle convalide
// 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. Gestione degli errori
// 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; } }
Lista di controllo per l'implementazione
Prima dello sviluppo
- Mappa tutti i campi obbligatori
- Definire le regole di convalida
- Pianificare il flusso di riempimento
- Identificare i campi interdipendenti
Durante lo sviluppo
- Implementare convalide sincrone
- Aggiungi convalide asincrone
- Configura il feedback visivo
- Prova diversi scenari
Post-sviluppo
- Controllare accessibilità
- Prova su diversi dispositivi
- Convalida le prestazioni
- Documentare comportamenti speciali
Conclusione
La creazione di moduli complessi con React Hook Form richiede un equilibrio tra funzionalità ed esperienza utente. Le migliori pratiche includono:
- Feedback immediato: convalida in tempo reale e messaggistica chiara
- Accessibilità: supporta la navigazione tramite tastiera e gli screen reader
- Prestazioni: convalida ottimizzata e rendering efficiente
- Usabilità: interfaccia intuitiva e prevenzione degli errori
- Manutenibilità: codice organizzato e ben documentato
Passaggi successivi
- Implementa gli esempi nel tuo progetto
- Adatta le impostazioni predefinite alle tue esigenze
- Raccogli il feedback degli utenti
- Iterare e migliorare continuamente
Stai sviluppando forme complesse? Condividi le tue esperienze e domande nei commenti qui sotto!
Leggi anche
- Sistemi di progettazione moderna: creazione di librerie di componenti scalabili con React e TypeScript
- Buone pratiche di internazionalizzazione (i18n) in React e Next.js nel 2025
- Accessibilità digitale UX
- Accessibilità nelle applicazioni mobili
- Design centrato sull'utente: come scegliere la scalabilità
- Interaction Design: come scegliere con la checklist
