tutoriales·7 min de lectura

Formularios Dinamicos con React Hook Form y Zod

Crea formularios dinamicos con campos condicionales, arrays de campos y validacion type-safe usando React Hook Form y Zod en Next.js.

Formularios Dinamicos con React Hook Form y Zod

Formularios dinamicos -- donde los campos cambian segun lo que el usuario selecciona o donde puedes agregar y quitar campos -- son de los patrones mas comunes en apps reales. React Hook Form con Zod te da la mejor combinacion de rendimiento y type-safety para resolverlos.

Si no has usado esta combinacion antes, arranca con la guia basica de validacion con Zod y React Hook Form. Aqui vamos directo a los patrones avanzados.

Setup rapido

bash
npm install react-hook-form @hookform/resolvers zod

Campos condicionales

El caso clasico: si el usuario selecciona "empresa", aparece el campo RFC. Si selecciona "persona", no.

typescript
"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 valida automaticamente segun 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 multiples items -- lineas de factura, experiencia laboral, ingredientes de una receta:

typescript
"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({
    descripcion: z.string().min(1, "Descripcion requerida"),
    cantidad: z.number().min(1, "Minimo 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: [{ descripcion: "", 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}.descripcion`)}
            placeholder="Descripcion"
          />
          <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({ descripcion: "", 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 valida cada item del array automaticamente.

Validacion cruzada con superRefine

Cuando un campo depende del valor de otro:

typescript
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:

typescript
// 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 validacion, un solo schema, zero duplicacion. Si quieres profundizar en Zod, la guia de validacion con Zod cubre todo desde lo basico hasta patrones avanzados.

#react-hook-form#zod#formularios#typescript#react#validacion

Preguntas frecuentes

Como agrego campos dinamicos con React Hook Form?

Con el hook useFieldArray. Le pasas el nombre del campo que es un array en tu schema, y te da funciones append, remove y fields para agregar, quitar y renderizar campos dinamicamente.

Como hago validacion 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 mas liviano, mas rapido, 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 validacion con un solo schema.