Membangun form registrasi yang type-safe di Next.js dengan React Hook Form dan skema Zod — mulai dari setup, validasi, sampai memasang komponennya ke sebuah halaman.

Form adalah bagian tak terpisahkan dari aplikasi web modern, dan mengelola state serta validasinya secara efisien sering kali jadi tantangan tersendiri. Di sinilah React Hook Form unggul. Pada artikel ini kita akan membahas cara mengintegrasikan React Hook Form ke dalam proyek Next.js menggunakan TypeScript demi type safety dan pengalaman pengembangan yang lebih baik.
React Hook Form (RHF) menyederhanakan penanganan form di aplikasi React dengan menyediakan:
Dipadukan dengan Next.js, RHF membuat pembuatan form terasa mulus, bahkan pada aplikasi kompleks yang di-render di server.
Pertama, buat proyek Next.js baru dengan dukungan TypeScript:
npx create-next-app@latest my-nextjs-app --typescript
cd my-nextjs-appSelanjutnya, pasang dependensi yang dibutuhkan:
npm install react-hook-form @hookform/resolvers zodMari kita buat form registrasi sederhana dengan field berikut:
Dengan Zod, definisikan skema validasinya:
import { z } from 'zod';
export const registrationSchema = z.object({
name: z.string().nonempty('Name is required'),
email: z.string().email('Invalid email address'),
password: z.string().min(6, 'Password must be at least 6 characters long'),
repeatPassword: z.string().min(6, 'Repeat Password must be at least 6 characters long'),
}).refine((data) => data.password === data.repeatPassword, {
message: 'Passwords do not match',
path: ['repeatPassword'],
});
export type RegistrationFormValues = z.infer<typeof registrationSchema>;Buat komponen RegistrationForm:
import { useForm, SubmitHandler } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { registrationSchema, RegistrationFormValues } from "./schemas/registrationSchema";
const RegistrationForm = () => {
const form = useForm<RegistrationFormValues>({
resolver: zodResolver(registrationSchema),
});
const errors = form.formState.errors;
const onSubmit: SubmitHandler<RegistrationFormValues> = (data) => {
console.log(data);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Name
</label>
<input
id="name"
type="text"
{...form.register("name")}
className={`mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm ${
errors.name ? "border-red-500" : ""
}`}
/>
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name.message}</p>}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
id="email"
type="email"
{...form.register("email")}
className={`mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm ${
errors.email ? "border-red-500" : ""
}`}
/>
{errors.email && <p className="mt-1 text-sm text-red-500">{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<input
id="password"
type="password"
{...form.register("password")}
className={`mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm ${
errors.password ? "border-red-500" : ""
}`}
/>
{errors.password && <p className="mt-1 text-sm text-red-500">{errors.password.message}</p>}
</div>
<div>
<label htmlFor="repeatPassword" className="block text-sm font-medium text-gray-700">
Repeat Password
</label>
<input
id="repeatPassword"
type="password"
{...form.register("repeatPassword")}
className={`mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm ${
errors.repeatPassword ? "border-red-500" : ""
}`}
/>
{errors.repeatPassword && (
<p className="mt-1 text-sm text-red-500">{errors.repeatPassword.message}</p>
)}
</div>
<button
type="submit"
className="w-full rounded-md bg-indigo-600 py-2 px-4 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Register
</button>
</form>
);
};
export default RegistrationForm;Buat halaman baru di app/register/page.tsx:
import RegistrationForm from '../../components/RegistrationForm';
const RegisterPage = () => {
return (
<div className="mx-auto max-w-md py-10">
<h1 className="text-2xl font-bold text-center mb-6">Register</h1>
<RegistrationForm />
</div>
);
};
export default RegisterPage;Jalankan development server:
npm run devBuka http://localhost:3000/register untuk melihat formnya bekerja. Coba kirim form dengan input yang tidak valid untuk melihat pesan validasinya.
Memadukan React Hook Form dengan Next.js dan TypeScript menghasilkan kombinasi yang ampuh untuk membangun form yang tangguh dan nyaman dipakai. Dengan boilerplate yang minim, type safety yang kuat, dan performa yang baik, RHF membuat developer bisa fokus menciptakan pengalaman pengguna yang baik.
Coba di proyek Anda berikutnya dan rasakan bedanya!

Panduan membuat portfolio sepenuhnya dwibahasa (EN/ID) — subpath routing dengan next-intl, localization konten per-field di Payload CMS, serta jebakan 404 dan SEO yang saya temui sepanjang jalan.

Aplikasi Next.js yang merender QR code sambil Anda mengetik dan membebaskan Anda mengatur setiap bagiannya — titik, sudut, warna, logo di tengah — lalu mengekspornya ke PNG, JPEG, SVG, atau WEBP.

Komponen AnimatedSection yang bisa dipakai ulang untuk menganimasikan konten saat masuk dan keluar viewport, mengikuti arah dan posisi scroll, dibangun dengan Framer Motion dan React hooks.