Formularios dinámicos con React Hook Form y Zod
Crea formularios dinámicos con campos condicionales, arrays de campos y validación type-safe usando React Hook Form y Zod en Next.js.
Formularios dinámicos con React Hook Form y Zod
Formularios dinámicos -- donde los campos cambian según lo que el usuario selecciona o donde puedes agregar y quitar campos -- son de los patrones más comunes en apps reales. React Hook Form con Zod te da la mejor combinación de rendimiento y type-safety para resolverlos.
Si no has usado esta combinación antes, arranca con la guía básica de validación con Zod y React Hook Form. aquí vamos directo a los patrones avanzados.
Setup rápido
npm install react-hook-form @hookform/resolvers zodCampos condicionales
El caso clásico: si el usuario selecciona "empresa", aparece el campo RFC. Si selecciona "persona", no.
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.discriminatedUnion("tipo", [
z.object({
tipo: z.literal("persona"),
nombre: z.string().min(2, "Nombre requerido"),
email: z.string().email("Email invalido"),
}),
z.object({
tipo: z.literal("empresa"),
nombre: z.string().min(2, "Nombre requerido"),
email: z.string().email("Email invalido"),
rfc: z.string().length(13, "RFC debe tener 13 caracteres"),
razonSocial: z.string().min(5, "Razon social requerida"),
}),
]);
type FormData = z.infer<typeof schema>;
export default function RegistroForm() {
const { register, handleSubmit, watch, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { tipo: "persona" },
});
const tipo = watch("tipo");
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<select {...register("tipo")}>
<option value="persona">Persona</option>
<option value="empresa">Empresa</option>
</select>
<input {...register("nombre")} placeholder="Nombre" />
{errors.nombre && <span>{errors.nombre.message}</span>}
<input {...register("email")} placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
{tipo === "empresa" && (
<>
<input {...register("rfc")} placeholder="RFC" />
{errors.rfc && <span>{errors.rfc.message}</span>}
<input {...register("razonSocial")} placeholder="Razon Social" />
{errors.razonSocial && <span>{errors.razonSocial.message}</span>}
</>
)}
<button type="submit">Registrar</button>
</form>
);
}discriminatedUnion en Zod válida automáticamente según el valor de tipo. Si es "empresa", exige RFC y razon social. Si es "persona", no. Todo type-safe.
Arrays de campos (useFieldArray)
Para formularios donde el usuario agrega múltiples items -- líneas de factura, experiencia laboral, ingredientes de una receta:
"use client";
import { useForm, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
cliente: z.string().min(2),
items: z.array(z.object({
descripción: z.string().min(1, "Descripción requerida"),
cantidad: z.number().min(1, "mínimo 1"),
precio: z.number().min(0, "Precio invalido"),
})).min(1, "Agrega al menos un item"),
});
type FormData = z.infer<typeof schema>;
export default function FacturaForm() {
const { register, control, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
items: [{ descripción: "", cantidad: 1, precio: 0 }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "items",
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("cliente")} placeholder="Cliente" />
{fields.map((field, index) => (
<div key={field.id} className="flex gap-2">
<input
{...register(`items.${index}.descripción`)}
placeholder="Descripción"
/>
<input
{...register(`items.${index}.cantidad`, { valueAsNumber: true })}
type="number"
placeholder="Cant"
/>
<input
{...register(`items.${index}.precio`, { valueAsNumber: true })}
type="number"
placeholder="Precio"
/>
{fields.length > 1 && (
<button type="button" onClick={() => remove(index)}>
Quitar
</button>
)}
</div>
))}
<button
type="button"
onClick={() => append({ descripción: "", cantidad: 1, precio: 0 })}
>
Agregar item
</button>
<button type="submit">Crear factura</button>
</form>
);
}useFieldArray te da append, remove, move, swap e insert. El schema de Zod válida cada item del array automáticamente.
Validación cruzada con superRefine
Cuando un campo depende del valor de otro:
const schema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Las contrasenas no coinciden",
path: ["confirmPassword"],
});
}
});Reutilizar el schema en el servidor
La ventaja de Zod es que el mismo schema funciona en cliente y servidor:
// lib/schemas/registro.ts -- un solo archivo
export const registroSchema = z.object({ /* ... */ });
// En el componente (cliente)
const form = useForm({ resolver: zodResolver(registroSchema) });
// En la Server Action (servidor)
export async function registrar(formData: FormData) {
const parsed = registroSchema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { errors: parsed.error.flatten() };
// ... guardar en DB
}Doble validación, un solo schema, zero duplicación. Si quieres profundizar en Zod, la guía de validación con Zod cubre todo desde lo básico hasta patrones avanzados.
Preguntas frecuentes
¿Cómo agrego campos dinámicos con React Hook Form?
Con el hook useFieldArray. Le pasas el nombre del campo qué es un array en tu schema, y te da funciones append, remove y fields para agregar, quitar y renderizar campos dinamicamente.
¿Cómo hago validación condicional con Zod?
Con discriminatedUnion o con refine/superRefine. Si un campo depende del valor de otro (por ejemplo, si seleccionas 'empresa' debes llenar RFC), usas superRefine para validar condicionalmente.
¿React Hook Form o Formik en 2026?
React Hook Form. Es más liviano, más rápido, y se integra mejor con TypeScript y Zod. Formik esta en modo mantenimiento sin actualizaciones significativas desde 2023.
¿Puedo usar React Hook Form con Server Actions?
Si. Puedes validar con Zod en el cliente con React Hook Form, y re-validar en el servidor con el mismo schema de Zod en tu Server Action. Doble validación con un solo schema.
Articulos relacionados
Zod Avanzado: Discriminated Unions, Transforms y Pipes
Patrones avanzados de Zod: discriminated unions, transforms, pipes, preprocess, y como validar datos complejos en TypeScript con schemas reutilizables.
tRPC + Next.js: APIs Type-Safe sin REST
Implementa tRPC en Next.js para APIs 100% type-safe. Sin schemas de API, sin fetch manual, sin types duplicados. End-to-end type safety con TypeScript.
Webhooks en Next.js: Recibe y Procesa Eventos
Implementa webhooks en Next.js para recibir eventos de Stripe, GitHub, Clerk y otros servicios. Verificación de firmas, tipado y manejo de errores.