Use Cases (Actualizado: 2/6/2026)

React Hook Form con Claude Code: guía segura de implementación

Aprende a crear formularios con React Hook Form, Zod, errores accesibles, estados de envío, pruebas y Claude Code.

React Hook Form con Claude Code: guía segura de implementación

Define el contrato del formulario antes de pedir código

React Hook Form es una biblioteca ligera para construir formularios en React. En vez de guardar cada tecla en useState, aprovecha el comportamiento nativo del navegador y ofrece APIs como register, handleSubmit y formState. Para alguien que empieza, esto aclara tres dudas importantes: dónde se recogen los valores, cuándo se ejecuta la validación y cómo se evita un segundo envío mientras la petición sigue en curso.

Claude Code puede generar el componente, el schema de Zod, la ruta de API, las pruebas y una refactorización inicial en una sola pasada. Eso ahorra tiempo, pero un formulario suele estar en una zona crítica del negocio: contacto comercial, prueba de producto, encuesta antes de comprar, registro a newsletter o edición de perfil. Si solo pides “crea un formulario”, puedes recibir una interfaz bonita que no valida en el servidor, no muestra errores accesibles o no bloquea el botón durante el envío.

Esta guía usa un formulario de consulta para explicar useForm, zodResolver, errores por campo, estado de envío, revalidación en API, pruebas y prompts seguros para Claude Code. Para patrones de React más amplios, revisa desarrollo React con Claude Code. Para diseño de schemas, acompáñalo con validación Zod con Claude Code.

Arquitectura: el schema en el centro

React Hook Form gestiona el flujo del formulario. Zod define qué datos son válidos. zodResolver, del paquete @hookform/resolvers/zod, conecta ambos para que React Hook Form ejecute el schema en el momento de validar.

flowchart TD
  A["La persona escribe"] --> B["React Hook Form register"]
  B --> C["zodResolver valida el schema"]
  C --> D{"Datos válidos"}
  D -->|No| E["Mostrar errores de campo"]
  D -->|Sí| F["handleSubmit envía valores"]
  F --> G["La API valida el mismo schema"]
  G --> H["Guardar, notificar o sincronizar CRM"]

En palabras simples, useForm es el controlador, el schema de Zod es el reglamento y el resolver es el adaptador. Al pedir cambios a Claude Code, nombrar estas piezas reduce el riesgo de que reescriba más de lo necesario. También permite pedir “actualiza schema, opciones del select, API y pruebas al mismo tiempo”.

Para comprobar detalles actuales, usa fuentes oficiales: useForm de React Hook Form, React Hook Form Resolvers, API de Zod, referencia de React para <input>, y documentación de Claude Code: overview y commands.

Schema Zod listo para copiar

Empieza separando las reglas de validación en un archivo propio. El ejemplo incluye nombre, email, categoría, mensaje y consentimiento de contacto. z.infer genera el tipo TypeScript desde el schema, así evitas mantener una interfaz distinta de la validación real.

// src/features/inquiry/inquirySchema.ts
import { z } from "zod";

export const inquirySchema = z.object({
  name: z
    .string()
    .trim()
    .min(1, "Escribe tu nombre")
    .max(80, "El nombre debe tener como máximo 80 caracteres"),
  email: z
    .string()
    .trim()
    .email("Escribe un email válido"),
  category: z.enum(["consulting", "support", "billing"], {
    error: "Elige una categoría",
  }),
  message: z
    .string()
    .trim()
    .min(10, "Escribe al menos 10 caracteres")
    .max(1000, "El mensaje debe tener como máximo 1000 caracteres"),
  agreeToContact: z.boolean().refine((value) => value, {
    message: "Debes aceptar que te contactemos",
  }),
});

export type InquiryFormValues = z.infer<typeof inquirySchema>;

La categoría es un enum porque el valor enviado debe ser estable. En proyectos reales, ese valor puede decidir si el lead va a ventas, soporte, facturación o un flujo de CRM. En el prompt para Claude Code, conviene escribir tanto la etiqueta visible como el valor enviado: “etiqueta: Soporte técnico, valor: support”. La etiqueta puede traducirse; el valor debe mantenerse estable.

useForm para valores, errores y envío

Este componente conecta el schema con React Hook Form. mode: "onBlur" valida cuando la persona sale del campo. Para formularios de contacto, suele ser menos agresivo que mostrar errores en cada tecla. En el envío final, handleSubmit vuelve a validar antes de llamar a onSubmit.

// src/features/inquiry/InquiryForm.tsx
"use client";

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { inquirySchema, type InquiryFormValues } from "./inquirySchema";

async function sendInquiry(values: InquiryFormValues) {
  const response = await fetch("/api/inquiry", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(values),
  });

  if (!response.ok) {
    throw new Error("Failed to send inquiry");
  }
}

export function InquiryForm() {
  const {
    register,
    handleSubmit,
    reset,
    setError,
    formState: { errors, isSubmitting },
  } = useForm<InquiryFormValues>({
    resolver: zodResolver(inquirySchema),
    mode: "onBlur",
    defaultValues: {
      name: "",
      email: "",
      message: "",
      agreeToContact: false,
    },
  });

  const onSubmit = async (values: InquiryFormValues) => {
    try {
      await sendInquiry(values);
      reset();
    } catch {
      setError("root", {
        type: "server",
        message: "No pudimos enviar el formulario. Inténtalo de nuevo en un momento.",
      });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <div>
        <label htmlFor="name">Nombre</label>
        <input
          id="name"
          autoComplete="name"
          aria-invalid={errors.name ? "true" : "false"}
          aria-describedby={errors.name ? "name-error" : undefined}
          {...register("name")}
        />
        {errors.name && (
          <p id="name-error" role="alert">
            {errors.name.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          autoComplete="email"
          aria-invalid={errors.email ? "true" : "false"}
          aria-describedby={errors.email ? "email-error" : undefined}
          {...register("email")}
        />
        {errors.email && (
          <p id="email-error" role="alert">
            {errors.email.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="category">Tema</label>
        <select
          id="category"
          aria-invalid={errors.category ? "true" : "false"}
          aria-describedby={errors.category ? "category-error" : undefined}
          {...register("category")}
        >
          <option value="">Elige una opción</option>
          <option value="consulting">Consultoría de implementación</option>
          <option value="support">Soporte técnico</option>
          <option value="billing">Facturación o contrato</option>
        </select>
        {errors.category && (
          <p id="category-error" role="alert">
            {errors.category.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="message">Mensaje</label>
        <textarea
          id="message"
          rows={6}
          aria-invalid={errors.message ? "true" : "false"}
          aria-describedby={errors.message ? "message-error" : undefined}
          {...register("message")}
        />
        {errors.message && (
          <p id="message-error" role="alert">
            {errors.message.message}
          </p>
        )}
      </div>

      <label>
        <input type="checkbox" {...register("agreeToContact")} />
        Acepto que me contacten sobre esta consulta
      </label>
      {errors.agreeToContact && (
        <p role="alert">{errors.agreeToContact.message}</p>
      )}

      {errors.root && <p role="alert">{errors.root.message}</p>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Enviando..." : "Enviar consulta"}
      </button>
    </form>
  );
}

La parte importante no es el estilo, sino la relación entre campo y error. Cada campo usa aria-invalid, cada mensaje usa role="alert" y aria-describedby une el input con su explicación. Esto mejora accesibilidad y también hace las pruebas más claras. Para ampliar, consulta accesibilidad con Claude Code.

Revalidar también en la API

La validación del cliente mejora la experiencia, pero no es una barrera de seguridad. Una persona puede saltarse el formulario y llamar a la API directamente. Por eso la API debe reutilizar el mismo schema antes de guardar, enviar email o sincronizar un CRM.

// app/api/inquiry/route.ts
import { NextResponse } from "next/server";
import { inquirySchema } from "@/features/inquiry/inquirySchema";

export async function POST(request: Request) {
  const payload = await request.json().catch(() => null);
  const parsed = inquirySchema.safeParse(payload);

  if (!parsed.success) {
    return NextResponse.json(
      {
        error: "Invalid inquiry",
        fields: parsed.error.flatten().fieldErrors,
      },
      { status: 400 },
    );
  }

  // TODO: Guardar en base de datos, enviar email o sincronizar CRM.
  return NextResponse.json({ ok: true });
}

Al pedir esta ruta a Claude Code, sé específico: “reutiliza inquirySchema, devuelve 400 con errores por campo y deja email o CRM como TODO”. Así la primera versión es revisable. Secretos, reintentos y deduplicación deben tratarse como tareas separadas.

Pruebas para no romper el formulario

Los formularios se rompen de formas poco visibles. Como mínimo, prueba envío vacío, envío válido, fallo de servidor y desactivación del botón. Con Vitest y React Testing Library puedes verificar errores y llamada a fetch.

// src/features/inquiry/InquiryForm.test.tsx
import { afterEach, expect, test, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { InquiryForm } from "./InquiryForm";

afterEach(() => {
  vi.unstubAllGlobals();
});

test("muestra errores al enviar vacío", async () => {
  render(<InquiryForm />);

  await userEvent.click(screen.getByRole("button", { name: "Enviar consulta" }));

  expect(await screen.findAllByRole("alert")).toHaveLength(5);
});

test("envía valores válidos a la API", async () => {
  const fetchMock = vi.fn().mockResolvedValue(new Response("{}", { status: 200 }));
  vi.stubGlobal("fetch", fetchMock);
  render(<InquiryForm />);

  await userEvent.type(screen.getByLabelText("Nombre"), "Masa");
  await userEvent.type(screen.getByLabelText("Email"), "masa@example.com");
  await userEvent.selectOptions(screen.getByLabelText("Tema"), "consulting");
  await userEvent.type(
    screen.getByLabelText("Mensaje"),
    "Quiero implementar React Hook Form de forma segura.",
  );
  await userEvent.click(
    screen.getByLabelText("Acepto que me contacten sobre esta consulta"),
  );
  await userEvent.click(screen.getByRole("button", { name: "Enviar consulta" }));

  expect(fetchMock).toHaveBeenCalledWith(
    "/api/inquiry",
    expect.objectContaining({ method: "POST" }),
  );
});

Puedes pedir a Claude Code que escriba primero las pruebas que fallan y después implemente. Para flujos completos, conecta esto con pruebas Playwright con Claude Code. Para métricas de negocio, registra el evento después de que la API acepte el formulario, no solo al hacer clic; esa idea encaja con analítica con Claude Code.

Prompt seguro para Claude Code

Un buen prompt de formulario incluye alcance, restricciones, comandos de verificación y no objetivos.

Implementa un formulario de consulta con React Hook Form y Zod.

Alcance:
- Edita solo src/features/inquiry y app/api/inquiry.
- Usa useForm, zodResolver y tipos TypeScript derivados del schema.
- Campos: name, email, category, message, agreeToContact.
- Muestra errores con role="alert" y aria-describedby.
- Desactiva el botón mientras isSubmitting sea true.
- Revalida el mismo schema Zod en la API.
- Añade pruebas con Vitest + Testing Library.

Verificación:
- npm test -- InquiryForm
- npm run typecheck

No hacer:
- No añadir una nueva librería UI.
- No renombrar valores existentes de category.
- No implementar email real, CRM ni secretos en esta tarea.

Para cambios pequeños, escribe el contrato completo. No digas solo “agrega otra categoría”; di “agrega la etiqueta Solicitud de formación con valor training, y actualiza enum, select, API, pruebas y mapeo de analítica”. Claude Code puede encontrar archivos relacionados, pero la definición del contrato debe venir de la persona responsable.

Casos de uso y diferencias

Caso de usoEstructura adecuadaCuidado con
Formulario de contactoZod + React Hook Form + revalidación APIMedir leads completados, no clics
Edición de perfilCargar datos en defaultValuesTras guardar, llamar reset(savedValues)
Encuesta antes de comprarCombinar select, radio y checkboxAlinear valores con producto o CRM
Búsqueda de administraciónValidación ligera y URL queryNo llamar API en cada tecla

La regla común es separar etiqueta visual y valor enviado. Las etiquetas se traducen y se reescriben. Los valores enviados deben ser estables porque informes, automatizaciones y backend dependen de ellos.

Errores frecuentes

El primero es validar solo en el navegador. Importa el schema compartido en la API y llama a safeParse antes de procesar el payload.

El segundo es perder isSubmitting demasiado pronto. Si onSubmit no hace await del trabajo asíncrono, el botón puede habilitarse antes de tiempo.

El tercero es mostrar errores lejos del campo. Un banner superior puede ayudar, pero cada campo necesita su mensaje cercano y conectado.

El cuarto es dejar que Claude Code invente un nuevo sistema visual. Si tu proyecto ya tiene TextField, Select, Button o toast, indícalo en el prompt.

El quinto es olvidar lo que ocurre después del envío: mensaje de éxito, página de gracias, email, evento de analítica y CRM deben planificarse.

CTA de monetización

La calidad de un formulario se mide por el embudo que sostiene. Antes de refactorizar, decide el evento de negocio: descarga de PDF, lead de producto, compra de plantilla o solicitud de consultoría.

Para avanzar por tu cuenta, revisa los materiales en productos. Si quieres aplicar Claude Code a formularios de equipo, empieza por formación y consultoría. Un formulario es pequeño, pero suele ser la puerta entre contenido útil e ingresos.

Resultado probado

Masa probó esta estructura en un flujo pequeño de consultas. Lo más valioso fue centralizar el schema, porque evitó añadir una opción en el select y olvidar el valor permitido en la API. También ayudaron las pruebas de envío vacío y envío válido. Después de pedir cambios a Claude Code, esas pruebas detectaron rápido mensajes de error perdidos y una llamada fetch rota. En la práctica, mantener el formulario como contrato de entrada es más sólido que tratarlo solo como UI.

#Claude Code #React Hook Form #React #formularios #validación
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.