Validasi Antar Field Dengan Zod Dan React Hook Form
Dalam pengembangan form modern, validasi jarang berdiri sendiri pada satu field. Hampir selalu ada kebutuhan seperti:
- Konfirmasi password harus sama dengan password
- End date harus lebih besar dari start date
- Field tertentu wajib diisi jika checkbox aktif
- Struktur field berubah tergantung pilihan user
Artikel ini membahas secara mendalam bagaimana melakukan validasi berdasarkan field lain menggunakan Zod dan React Hook Form (RHF), termasuk pendekatan sederhana hingga arsitektur yang scalable untuk form kompleks.
Konsep Dasar: Cross-Field Validation
Cross-field validation adalah validasi yang membutuhkan lebih dari satu field untuk menentukan valid atau tidaknya suatu nilai.
Contoh sederhana:
confirmPasswordvalid hanya jika sama denganpasswordcompanyNamerequired hanya jikaisCompany = true
Zod menyediakan beberapa mekanisme untuk ini:
.refine()→ untuk validasi sederhana.superRefine()→ untuk validasi kompleks dengan kontrol penuhz.discriminatedUnion()→ untuk struktur conditional berbasis enum
Menggunakan .refine() (Kasus Sederhana)
Gunakan .refine() ketika:
- Hanya ada satu kondisi
- Hanya perlu satu error
- Tidak perlu multiple issue dalam satu validasi
Contoh: Konfirmasi Password
import { z } from "zod";
const schema = z
.object({
password: z.string().min(8),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Password tidak sama",
path: ["confirmPassword"],
});
Kenapa path penting?
Tanpa path, error akan muncul di level root form.
Dengan path, error diarahkan langsung ke field tertentu.
Menggunakan .superRefine() (Kontrol Penuh)
Gunakan .superRefine() ketika:
- Ada banyak kondisi
- Bisa muncul multiple error sekaligus
- Perlu kontrol granular terhadap issue
Contoh: Conditional Required Field
const schema = z.object({
isCompany: z.boolean(),
companyName: z.string().optional(),
}).superRefine((data, ctx) => {
if (data.isCompany && !data.companyName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Nama perusahaan wajib diisi",
path: ["companyName"],
});
}
});
Kenapa pakai .optional()?
Karena jika companyName selalu required di schema utama, validasi akan gagal sebelum conditional logic dijalankan.
Validasi Relasi Antar Nilai (Date Range)
Contoh umum dalam sistem booking, event, atau filter:
const schema = z.object({
startDate: z.date(),
endDate: z.date(),
}).refine((data) => data.endDate > data.startDate, {
message: "End date harus setelah start date",
path: ["endDate"],
});
Best Practice
Selalu arahkan error ke field yang dianggap “salah”, biasanya field kedua.
Integrasi dengan React Hook Form
Zod digunakan melalui resolver:
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const form = useForm({
resolver: zodResolver(schema),
mode: "onChange",
});
Catatan Penting
Jika menggunakan form dynamic (show/hide field):
- Field tetap divalidasi walaupun tidak dirender
- Gunakan
.optional()untuk field conditional - Atau gunakan discriminated union (lebih scalable)
Arsitektur Lebih Scalable: discriminatedUnion
Ketika satu field menentukan struktur form secara keseluruhan, gunakan pendekatan ini.
Contoh: Payment Method
const schema = z.discriminatedUnion("paymentMethod", [
z.object({
paymentMethod: z.literal("credit_card"),
cardNumber: z.string().min(1),
}),
z.object({
paymentMethod: z.literal("bank_transfer"),
bankAccount: z.string().min(1),
}),
]);
Kenapa Ini Lebih Baik?
- Tidak perlu conditional manual
- Tidak perlu
.optional()workaround - Struktur type lebih bersih
- Lebih aman untuk refactor
- Lebih cocok untuk form kompleks atau multi-step
Kapan Menggunakan Apa?
| Kebutuhan | Gunakan |
|---|---|
| Validasi 1 kondisi sederhana | refine |
| Banyak kondisi & multiple error | superRefine |
| Struktur berubah berdasarkan enum | discriminatedUnion |
| Form sangat dynamic | discriminatedUnion + optional |
Kesalahan Umum yang Sering Terjadi
1. Lupa .optional() pada field conditional
Field tetap dianggap required walaupun secara UI disembunyikan.
2. Error muncul di root form
Karena tidak menggunakan path.
3. Logic terlalu banyak di UI
Validasi seharusnya di schema, bukan di component.
4. Menggunakan superRefine untuk semua hal
Padahal discriminatedUnion lebih clean dan maintainable.
Perspektif Arsitektur
Cross-field validation bukan hanya soal teknis, tapi soal desain.
Pertanyaan yang perlu dipikirkan:
- Apakah validasi ini bagian dari domain rule?
- Apakah struktur data berubah berdasarkan state tertentu?
- Apakah validasi ini akan berkembang ke depan?
Jika jawabannya ya, maka pendekatan schema-based seperti Zod akan jauh lebih maintainable dibanding validasi manual di UI.
Penutup
Zod memberikan fleksibilitas penuh untuk melakukan validasi antar field, mulai dari kasus sederhana hingga struktur form kompleks.
Ringkasnya:
- Gunakan
refine()untuk kondisi sederhana - Gunakan
superRefine()untuk kontrol penuh - Gunakan
discriminatedUnion()untuk arsitektur scalable
Dengan pendekatan yang tepat, form complex sekalipun bisa tetap clean, predictable, dan maintainable.
Validasi bukan sekadar memastikan data benar — tetapi memastikan sistem tetap konsisten seiring pertumbuhan kompleksitas.