React
React Hook Form
UX Design
Formulários
Acessibilidade
TypeScript

Mejores prácticas de UX para formularios complejos con React Hook Form

Mejores prácticas de UX para formularios complejos con React Hook Form

En 2025 los formularios web seguirán siendo uno de los principales puntos de interacción entre usuarios y aplicaciones. Con React Hook Form evolucionando significativamente, exploremos las mejores prácticas para crear formularios complejos que no solo funcionan bien, sino que también brindan una experiencia de usuario excepcional.

Fundamentos de UX en formularios

Principios esenciales

  1. Comentarios inmediatos: validación en tiempo real
  2. Prevención de errores: Guíe al usuario antes de que ocurra un error
  3. Recuperación de errores: borrar mensajes y acciones correctivas
  4. Progresión natural: flujo de llenado lógico
  5. Accesibilidad: Admite diferentes necesidades

Implementación con formulario React Hook

Configuración inicial 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 de formulario 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> ); }

Campos inteligentes con comentarios

// 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> ); }

Formulario de varios pasos

// 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> ); }

Validación asincrónica

// 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]); }

Patrones de UX avanzados

1. Retroalimentación visual progresiva

// 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. Guardado automático

// 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. Navegación por teclado

// 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]); }

Mejores prácticas de implementación

1. Organización de Validaciones

// 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. Manejo de errores

// 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 de verificación de implementación

Antes del desarrollo

  • [] Asigna todos los campos obligatorios
  • [] Definir reglas de validación
  • Planificar el flujo de llenado
  • [] Identificar campos interdependientes

Durante el desarrollo

  • [] Implementar validaciones sincrónicas
  • [] Agregar validaciones asincrónicas
  • [] Configurar retroalimentación visual
  • [] Pruebe diferentes escenarios

Post-Desarrollo

  • Verifique accesibilidad
  • [] Prueba en diferentes dispositivos
  • [] Validar rendimiento
  • [] Documentar comportamientos especiales

Conclusión

La creación de formularios complejos con React Hook Form requiere un equilibrio entre funcionalidad y experiencia del usuario. Las mejores prácticas incluyen:

  1. Comentarios inmediatos: validación en tiempo real y mensajes claros
  2. Accesibilidad: admite navegación con teclado y lectores de pantalla
  3. Rendimiento: validación optimizada y renderizado eficiente
  4. Usabilidad: interfaz intuitiva y prevención de errores
  5. Mantenibilidad: código organizado y bien documentado

Próximos pasos

  1. Implemente los ejemplos en su proyecto.
  2. Adapta los valores predeterminados a tus necesidades
  3. Recopile comentarios de los usuarios
  4. Iterar y mejorar continuamente

¿Estás desarrollando formas complejas? ¡Comparta sus experiencias y preguntas en los comentarios a continuación!

Lea también