Form yang baik memeriksa isiannya sebelum diterima, lalu memberi tahu apa yang kurang tepat, tepat di kolom yang salah. Pemeriksaan ini namanya validasi. Bayangin petugas pendaftaran di loket. Kamu serahkan formulir, dia periksa. Kalau nama kosong, dia tunjuk kolomnya dan bilang "Nama wajib diisi". Kalau semuanya beres, dia cap "Diterima". Ada petugas yang memeriksa saat formulir diserahkan (saat kirim), ada juga yang ikut membaca sambil kamu menulis (sambil mengetik). Di React, dua-duanya kamu tulis sendiri dengan state.
🧪 Tebakan cepat: kamu kirim nama Budi dan muncul
Berhasil!. Lalu kamu hapus namanya tanpa menekan Kirim.Berhasil!masih tampil atau hilang? Tebak dulu, jawabannya ada di bagian "Periksa saat kirim".
Periksa saat kirim
function App() {
const [name, setName] = useState("")
const [error, setError] = useState("")
const [success, setSuccess] = useState(false)
const handleSubmit = (e) => {
e.preventDefault()
if (!name.trim()) {
setError("Nama wajib diisi")
setSuccess(false)
} else {
setError("")
setSuccess(true)
}
}
return (
<form onSubmit={handleSubmit}>
<input id="name" value={name} onChange={(e) => setName(e.target.value)} />
{error && <span id="error">{error}</span>}
{success && <span id="success">Berhasil!</span>}
<button type="submit">Kirim</button>
</form>
)
}
onSubmitdi<form>jalan saat tomboltype="submit"ditekan, atau saat Enter ditekan di kolomnya.e.preventDefault()mencegah browser memuat ulang halaman. Tanpa itu, semua state kembali kosong sebelum pesannya sempat tampil.name.trim()membuang spasi di awal dan akhir, jadi isian berupa spasi saja tetap dianggap kosong.- Hasil pemeriksaan disimpan di state
errordansuccess.{error && <span>...</span>}menampilkan pesannya hanya kalau ada isinya. - Tiap cabang mengatur keduanya: kiriman gagal mencabut sukses, kiriman sukses mengosongkan error. Jadi gak ada sisa dari kiriman sebelumnya.
Ini yang terjadi di kolom Nama di Lab, langkah demi langkah:
| Langkah | name |
error |
success |
Tampil |
|---|---|---|---|---|
| Kirim dengan kolom kosong | "" |
"Nama wajib diisi" |
false |
Nama wajib diisi |
| Ketik Budi | "Budi" |
"Nama wajib diisi" |
false |
Nama wajib diisi |
| Kirim | "Budi" |
"" |
true |
Berhasil! |
| Hapus namanya | "" |
"" |
true |
Berhasil! |
| Kirim lagi | "" |
"Nama wajib diisi" |
false |
Nama wajib diisi |
Mengetik cuma mengubah name. error dan success baru berubah saat kirim. Jadi setelah namanya dihapus, Berhasil! masih tampil, karena dia hasil kiriman terakhir. Itu jawaban tebakannya.
Periksa sambil mengetik
Kadang orang perlu tahu salahnya selagi mengetik, misalnya syarat password. Caranya: jangan simpan pesannya di state, hitung langsung dari isiannya saat render. Tiap ketikan memanggil setter, React render ulang, dan syaratnya diperiksa lagi.
function App() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
return (
<form>
<input id="email" value={email} onChange={(e) => setEmail(e.target.value)} />
{!email.includes("@") && <span id="email-error">Email harus mengandung @</span>}
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
{password.length < 6 && <span id="password-error">Password minimal 6 karakter</span>}
</form>
)
}
email.includes("@")bernilaitruekalau isiannya memuat@. Tanda!di depannya membalik: pesannya tampil selama@belum ada.password.length < 6bernilaitrueselama panjangnya kurang dari 6 karakter.syarat && <span>...</span>merender span-nya hanya kalau syaratnyatrue. Begitu isiannya sah, span-nya hilang dari halaman, bukan sekadar disembunyikan.
Ketik [email protected]: begitu @ masuk, pesan email lenyap. Ketik rahasiaku123: di huruf keenam, pesan password lenyap. Kekurangannya, pesan merahnya sudah muncul sebelum orang sempat mengetik apa pun. Jalan tengah yang sering dipakai: tampilkan pesan sebuah kolom setelah kolom itu ditinggalkan, lewat event onBlur. Coba ketiga caranya di Lab.
Banyak kolom, satu handler
Kamu sudah ketemu ini di materi Form: satu object state untuk semua kolom, dan satu handler untuk semuanya.
function DaftarForm() {
const [form, setForm] = useState({ name: "", email: "", password: "" })
function handleChange(e) {
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }))
}
return (
<form>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<input name="password" type="password" value={form.password} onChange={handleChange} />
</form>
)
}
- Tiap input diberi atribut
nameyang sama dengan nama field-nya di state. [e.target.name]adalah computed property: kunci object-nya diambil dari isi variabel. Mengetik di kolomname="email"berarti mengubahform.email....prevmenyalin field lain supaya gak ikut hilang.
Controlled atau uncontrolled
Semua contoh di atas adalah controlled component: nilainya disimpan di state React, lewat value dan onChange. Kebalikannya uncontrolled component: nilainya disimpan di DOM sendiri, dan kamu baru membacanya saat butuh, lewat ref.
function CariForm() {
const kotaRef = useRef(null)
function handleSubmit(e) {
e.preventDefault()
console.log("Cari:", kotaRef.current.value)
}
return (
<form onSubmit={handleSubmit}>
<input ref={kotaRef} defaultValue="Bandung" />
<button type="submit">Cari</button>
</form>
)
}
defaultValuecuma mengisi nilai awal. Setelah itu, browser yang memegang isiannya.kotaRef.current.valuemembaca isinya dari DOM saat form dikirim.- Gak ada state, jadi mengetik gak memicu render, dan gak bisa ada validasi sambil mengetik.
| Controlled | Uncontrolled | |
|---|---|---|
| Nilainya disimpan di | state React | DOM |
| Dibaca lewat | variabel state | ref, saat dibutuhkan |
| Validasi sambil mengetik | bisa | gak bisa |
| Cocok untuk | hampir semua form | form sederhana tanpa validasi real-time, atau library non-React yang mengurus input-nya sendiri |
📌 Kalau form-nya makin besar
Form 10 kolom dengan banyak aturan cepat jadi ratusan baris useState dan if. Di app sungguhan, orang biasanya memakai React Hook Form (mengurus nilai, error, dan status kirim) bersama Zod (aturan validasinya ditulis sekali sebagai schema). Dasarnya tetap yang kamu pelajari di sini: periksa, simpan hasilnya, tampilkan pesannya di dekat kolomnya.
⚠️ Jebakan yang sering ketemu
- Lupa
e.preventDefault(). Browser memuat ulang halaman, semua state kembali kosong, dan pesannya gak sempat terlihat. - Tombol tanpa
type="submit". Di dalam form,<button>tanpatypememang ikut mengirim, tapi tulistype="submit"supaya jelas. Pemeriksa latihan juga mencaributton[type="submit"]. - Menyembunyikan pesan dengan CSS.
style={{ display: "none" }}membuat elemennya tetap ada di halaman. Pakaisyarat && <span>...</span>supaya benar-benar hilang. - Kiriman gagal yang meninggalkan
Berhasil!. Kalau cabang gagal lupasetSuccess(false), pesan sukses yang lama tetap tampil di samping errornya. - Cuma memeriksa di browser. Orang bisa melewatinya lewat DevTools. Server tetap wajib memeriksa ulang. Validasi di React itu untuk kenyamanan pengguna.
TL;DR: Validasi saat kirim: di onSubmit, panggil e.preventDefault(), periksa isiannya, simpan hasilnya di state (error, success), dan atur dua-duanya di tiap cabang. Hasil itu bertahan sampai kiriman berikutnya, mengetik gak mengubahnya. Validasi sambil mengetik: hitung syaratnya langsung saat render (!email.includes("@"), password.length < 6) dan tampilkan pesannya dengan syarat && <span>, supaya hilang dari halaman begitu sah. onBlur adalah jalan tengahnya. Banyak kolom cukup satu handler dengan [e.target.name]. Controlled menyimpan nilai di state, uncontrolled menyimpannya di DOM dan dibaca lewat ref, cocok untuk form sederhana tanpa validasi real-time. Server tetap wajib memeriksa ulang.