Formularios
React Hook Form es una librería que automáticamente valida las entradas de los formularios, dando errores y evitando poner estado.
Sin RHF, manejar formularios en React requiere mucho código repetitivo:
// Sin RHF — todo manual
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [emailError, setEmailError] = useState('');
// ...validar a mano en cada onChange o onSubmit
Con RHF:
1. Define el schema con Zod
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email('Email inválido'),
password: z.string().min(1, 'Contraseña requerida'),
});
// TypeScript gratis — equivale a: { email: string; password: string }
type LoginValues = z.infer<typeof loginSchema>;
2. Inicializa el formulario
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const {
register, // conecta <input> al formulario
handleSubmit, // envuelve tu onSubmit con validación
formState: {
errors, // errores por campo
isSubmitting, // true mientras el submit está en curso
},
} = useForm<LoginValues>({
resolver: zodResolver(loginSchema), // conecta Zod
});
3. Conecta los inputs
<input
type="email"
aria-invalid={!!errors.email}
{...register('email')} // registra el campo: name, onChange, onBlur, ref
/>
{errors.email && <p>{errors.email.message}</p>}
4. Maneja el submit
async function onSubmit(data: LoginValues) {
// data ya está validado y tipado
await login.fetch({ body: data });
}
<form onSubmit={handleSubmit(onSubmit)}>
{/* handleSubmit ejecuta onSubmit SOLO si la validación pasa */}
Imágenes con ImageUpload + RHF
El componente ImageUpload (components/dondeSiempre/ImageUpload.tsx) recibe un callback onChange(file: File | null). Como no es un <input> nativo, no se puede usar register directamente — hay que usar Controller.
Con imagen obligatoria
import { useForm, Controller } from 'react-hook-form';
import { z } from 'zod';
import ImageUpload from '@/components/dondeSiempre/ImageUpload';
const schema = z.object({
name: z.string().min(1, 'Nombre requerido'),
// File no es serializable, usamos z.instanceof
image: z.instanceof(File, { message: 'La imagen es obligatoria' }),
});
type FormValues = z.infer<typeof schema>;
export function MyForm() {
const {
register,
control, // necesario para Controller
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register('name')} />
{errors.name && <p>{errors.name.message}</p>}
<Controller
name="image"
control={control}
render={({ field }) => (
<ImageUpload
onChange={(file) => field.onChange(file)} // conecta al formulario
/>
)}
/>
{errors.image && <p>{errors.image.message}</p>}
<button type="submit">Enviar</button>
</form>
);
}
Con imagen opcional
const schema = z.object({
name: z.string().min(1, 'Nombre requerido'),
image: z.instanceof(File).nullable().optional(),
// nullable() → acepta null (cuando el usuario no sube nada)
// optional() → acepta undefined (si el campo no aparece)
});
Con imagen opcional no necesitas Controller si prefieres useState como hacen PromotionForm.tsx y create-outfit/page.tsx:
const [imageFile, setImageFile] = useState<File | null>(null);
// En el submit, imageFile puede ser null — lo incluyes manualmente en el DTO
<ImageUpload onChange={setImageFile} />
Úsalo con Controller cuando quieras que RHF controle la validación de la imagen junto con el resto del formulario. Usa useState cuando la imagen sea opcional y no necesites validarla.