React Hook Form dengan Claude Code: implementasi yang aman
Bangun form React Hook Form dengan Zod, error yang aksesibel, status submit, testing, dan prompt Claude Code yang aman.
Tentukan kontrak form sebelum meminta Claude Code menulis kode
React Hook Form adalah library ringan untuk membuat form di React. Alih-alih menyimpan setiap ketikan di useState, library ini memanfaatkan perilaku native form di browser dan memberi API seperti register, handleSubmit, dan formState. Untuk pemula, ini membuat tiga hal lebih jelas: nilai dikumpulkan di mana, validasi berjalan kapan, dan tombol submit harus dinonaktifkan bagaimana saat request masih berjalan.
Claude Code bisa membuat komponen, schema Zod, route API, test, dan refactor awal dalam satu sesi. Namun form sering berada di jalur bisnis: permintaan konsultasi, trial produk, survei sebelum membeli, newsletter, atau edit profil. Jika instruksinya hanya “buatkan form”, hasilnya bisa terlihat rapi tetapi tidak memiliki pesan error yang aksesibel, validasi server, atau status submit yang andal.
Panduan ini memakai contoh inquiry form untuk membahas useForm, zodResolver, error per field, status submit, revalidasi API, testability, dan prompt Claude Code yang aman. Untuk pola React umum, baca pengembangan React dengan Claude Code. Untuk desain schema, lanjutkan ke validasi Zod dengan Claude Code.
Arsitektur: schema ada di pusat
React Hook Form mengatur workflow form. Zod mendeskripsikan input mana yang valid. zodResolver dari @hookform/resolvers/zod menghubungkan keduanya sehingga React Hook Form menjalankan schema Zod saat validasi.
flowchart TD
A["Pengguna mengisi field"] --> B["React Hook Form register"]
B --> C["zodResolver memvalidasi schema"]
C --> D{"Input valid"}
D -->|Tidak| E["Tampilkan error field"]
D -->|Ya| F["handleSubmit mengirim nilai"]
F --> G["API memvalidasi schema yang sama"]
G --> H["Simpan, kirim notifikasi, atau sync CRM"]
Sederhananya, useForm adalah controller, schema Zod adalah aturan, dan resolver adalah adapter. Saat meminta Claude Code mengubah form, menyebut tiga bagian ini mengurangi risiko rewrite yang tidak perlu. Saat menambah field baru, kamu bisa meminta schema, UI, API, dan test diperbarui bersama.
Untuk memastikan detail terbaru, gunakan sumber resmi: dokumentasi useForm, React Hook Form Resolvers, Zod API, referensi React untuk <input>, serta Claude Code overview dan commands.
Schema Zod yang bisa langsung disalin
Mulailah dengan memisahkan aturan validasi ke file sendiri. Contoh ini memiliki nama, email, kategori, pesan, dan persetujuan kontak. z.infer menghasilkan tipe TypeScript dari schema, sehingga tipe dan validasi runtime tidak dikelola terpisah.
// src/features/inquiry/inquirySchema.ts
import { z } from "zod";
export const inquirySchema = z.object({
name: z
.string()
.trim()
.min(1, "Masukkan nama")
.max(80, "Nama maksimal 80 karakter"),
email: z
.string()
.trim()
.email("Masukkan alamat email yang valid"),
category: z.enum(["consulting", "support", "billing"], {
error: "Pilih kategori",
}),
message: z
.string()
.trim()
.min(10, "Pesan minimal 10 karakter")
.max(1000, "Pesan maksimal 1000 karakter"),
agreeToContact: z.boolean().refine((value) => value, {
message: "Persetujuan kontak wajib diberikan",
}),
});
export type InquiryFormValues = z.infer<typeof inquirySchema>;
category dibuat enum karena nilai yang dikirim harus stabil. Dalam proyek nyata, nilai ini bisa menentukan lead masuk ke sales, support, billing, atau CRM. Di prompt Claude Code, tulis label dan nilai submit secara terpisah, misalnya “label: Dukungan teknis, value: support”. Label boleh diterjemahkan; value harus stabil.
useForm untuk nilai, error, dan status submit
Komponen berikut menghubungkan schema dengan React Hook Form. mode: "onBlur" berarti validasi berjalan ketika pengguna meninggalkan field. Untuk form kontak, ini biasanya lebih nyaman dibanding menampilkan error setiap kali mengetik. Saat submit, handleSubmit tetap menjalankan validasi final.
// 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: "Form belum bisa dikirim. Coba lagi beberapa saat lagi.",
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<div>
<label htmlFor="name">Nama</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">Topik</label>
<select
id="category"
aria-invalid={errors.category ? "true" : "false"}
aria-describedby={errors.category ? "category-error" : undefined}
{...register("category")}
>
<option value="">Pilih salah satu</option>
<option value="consulting">Konsultasi implementasi</option>
<option value="support">Dukungan teknis</option>
<option value="billing">Tagihan atau kontrak</option>
</select>
{errors.category && (
<p id="category-error" role="alert">
{errors.category.message}
</p>
)}
</div>
<div>
<label htmlFor="message">Pesan</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")} />
Saya setuju dihubungi terkait inquiry ini
</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 ? "Mengirim..." : "Kirim inquiry"}
</button>
</form>
);
}
Bagian pentingnya adalah hubungan field dan error. Setiap field memakai aria-invalid, setiap pesan memakai role="alert", dan aria-describedby menghubungkan input ke penjelasan error. Ini membantu pengguna screen reader sekaligus membuat test lebih mudah. Untuk topik lebih luas, baca aksesibilitas dengan Claude Code.
Validasi ulang di API
Validasi client meningkatkan UX, tetapi bukan batas keamanan. Request bisa dikirim langsung ke API tanpa melewati form. Karena itu API harus memakai schema yang sama sebelum menyimpan, mengirim email, atau sync ke 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: Simpan ke database, kirim email, atau sync CRM.
return NextResponse.json({ ok: true });
}
Saat meminta Claude Code menambahkan route, tulis jelas: “reuse inquirySchema, kembalikan 400 dengan field errors, dan biarkan email atau CRM sebagai TODO.” Secrets, retry, dan deduplikasi sebaiknya menjadi task terpisah.
Test perilaku penting
Form bisa rusak tanpa terlihat jelas. Minimal test empty submit, valid submit, server failure, dan tombol disabled. Dengan Vitest dan React Testing Library, kamu bisa memverifikasi error dan pemanggilan 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("menampilkan error saat submit kosong", async () => {
render(<InquiryForm />);
await userEvent.click(screen.getByRole("button", { name: "Kirim inquiry" }));
expect(await screen.findAllByRole("alert")).toHaveLength(5);
});
test("mengirim nilai valid ke API", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response("{}", { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
render(<InquiryForm />);
await userEvent.type(screen.getByLabelText("Nama"), "Masa");
await userEvent.type(screen.getByLabelText("Email"), "masa@example.com");
await userEvent.selectOptions(screen.getByLabelText("Topik"), "consulting");
await userEvent.type(
screen.getByLabelText("Pesan"),
"Saya ingin menerapkan React Hook Form dengan aman.",
);
await userEvent.click(
screen.getByLabelText("Saya setuju dihubungi terkait inquiry ini"),
);
await userEvent.click(screen.getByRole("button", { name: "Kirim inquiry" }));
expect(fetchMock).toHaveBeenCalledWith(
"/api/inquiry",
expect.objectContaining({ method: "POST" }),
);
});
Kamu bisa meminta Claude Code menulis failing tests terlebih dahulu, lalu implementasi. Untuk flow browser penuh, gunakan Playwright testing dengan Claude Code. Untuk tracking bisnis, kirim event setelah API menerima form, bukan saat tombol diklik; ini sejalan dengan analytics dengan Claude Code.
Prompt Claude Code yang aman
Prompt form yang baik memuat scope, constraints, command verifikasi, dan hal yang tidak dikerjakan.
Implementasikan inquiry form dengan React Hook Form dan Zod.
Scope:
- Hanya ubah src/features/inquiry dan app/api/inquiry.
- Gunakan useForm, zodResolver, dan TypeScript type dari schema.
- Field: name, email, category, message, agreeToContact.
- Tampilkan field errors dengan role="alert" dan aria-describedby.
- Disable submit button saat isSubmitting true.
- API route memakai schema Zod yang sama dengan safeParse.
- Tambahkan test Vitest + Testing Library.
Verifikasi:
- npm test -- InquiryForm
- npm run typecheck
Jangan:
- Jangan tambah UI library baru.
- Jangan rename value category yang sudah ada.
- Jangan implementasi email production, CRM, atau secret handling di task ini.
Untuk perubahan kecil, tetap tulis kontraknya. Jangan hanya bilang “tambahkan kategori”; tulis “tambahkan label Permintaan pelatihan dengan value training, lalu update enum, select, API validation, tests, dan analytics mapping.” Claude Code bisa mencari file terkait, tetapi kontrak harus jelas dari manusia.
Use case dan perbedaan desain
| Use case | Struktur yang baik | Perhatikan |
|---|---|---|
| Contact form | Zod + React Hook Form + API revalidation | Ukur lead sukses, bukan klik tombol |
| Edit profil | Masukkan data awal ke defaultValues | Setelah save, panggil reset(savedValues) |
| Survei sebelum membeli | Gabungkan select, radio, checkbox | Cocokkan value dengan produk atau CRM |
| Admin search | Validasi ringan dan sync URL query | Jangan panggil API di setiap ketikan |
Aturan umumnya: pisahkan label UI dan value yang dikirim. Label bisa diterjemahkan dan diubah. Value harus stabil karena laporan, automation, dan backend bergantung padanya.
Kesalahan umum
Pertama, hanya memvalidasi di browser. Import schema yang sama di API route dan panggil safeParse sebelum memproses payload.
Kedua, isSubmitting kembali false terlalu cepat. Jika onSubmit tidak await pekerjaan async, tombol bisa aktif lagi sebelum request selesai.
Ketiga, error terlalu jauh dari field. Banner di atas bisa membantu, tetapi setiap field tetap butuh pesan dekat dengan input.
Keempat, membiarkan Claude Code membuat design system baru. Jika aplikasi sudah punya TextField, Select, Button, atau toast, minta Claude Code menggunakannya.
Kelima, melupakan jalur setelah submit: success message, thank-you page, email, analytics event, dan CRM sync perlu direncanakan.
CTA monetisasi
Kualitas form diukur dari funnel yang didukungnya: signup PDF, lead produk, pembelian template, atau request konsultasi. Tentukan event bisnis dulu, baru minta Claude Code mengurangi field, memperbaiki copy error, atau menambah test.
Untuk belajar mandiri, lihat materi di halaman produk. Untuk menerapkan Claude Code pada workflow form tim, mulai dari training dan konsultasi. Form memang komponen kecil, tetapi sering menjadi gerbang antara konten berguna dan pendapatan.
Catatan hasil uji
Masa mencoba struktur ini pada inquiry flow kecil. Perubahan paling berguna adalah memusatkan schema, karena mencegah kesalahan menambah opsi di select tetapi lupa menambah allowed value di API. Test empty submit dan valid submit juga membantu. Setelah meminta Claude Code melakukan edit lanjutan, test itu cepat menangkap pesan error yang hilang dan panggilan fetch yang rusak. Dalam praktik, form lebih mudah dirawat jika dianggap sebagai kontrak input, bukan hanya UI.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.