Setelah memahami state dan hooks, saatnya membangun form dan event handler yang benar-benar fungsional. React menangani event dengan SyntheticEvent — wrapper di atas event browser native yang konsisten di semua browser. Memahami pola controlled component adalah kunci membuat form React yang dapat diprediksi dan mudah divalidasi.
Event Dasar: onClick, onMouseEnter, onKeyDown
function TombolInteraktif() {
const handleKlik = (e) => {
// e adalah SyntheticEvent — sama seperti event browser native
console.log("Diklik!", e.target, e.type);
};
const handleKeyDown = (e) => {
if (e.key === "Enter") {
console.log("Enter ditekan!");
}
if (e.key === "Escape") {
console.log("Escape ditekan!");
}
};
return (
<div>
<button
onClick={handleKlik}
onMouseEnter={() => console.log("Mouse masuk")}
onMouseLeave={() => console.log("Mouse keluar")}
>
Klik atau hover saya
</button>
<input
onKeyDown={handleKeyDown}
placeholder="Tekan Enter atau Escape"
/>
</div>
);
}
Penting: Tulis
onClick={handleKlik}bukanonClick={handleKlik()}. Tanda kurung()berarti fungsi dipanggil saat render, bukan saat klik!
Passing Argumen ke Event Handler
function DaftarMenu() {
const [dipilih, setDipilih] = useState(null);
const menu = ["Beranda", "Produk", "Tentang", "Kontak"];
// Cara 1: Arrow function (lebih umum)
return (
<ul>
{menu.map((item) => (
<li key={item}>
<button
onClick={() => setDipilih(item)} // Arrow function yang memanggil dengan argumen
style={{
fontWeight: dipilih === item ? "bold" : "normal",
color: dipilih === item ? "var(--color-primary)" : "inherit"
}}
>
{item}
</button>
</li>
))}
{dipilih && <p>Halaman: {dipilih}</p>}
</ul>
);
}
Mencegah Perilaku Default Browser
function FormCari() {
const [query, setQuery] = useState("");
const handleSubmit = (e) => {
e.preventDefault(); // Mencegah reload halaman (default form submit)
console.log("Mencari:", query);
// Lakukan pencarian...
};
const handleLink = (e, url) => {
e.preventDefault(); // Mencegah navigasi ke URL
console.log("Navigasi custom ke:", url);
};
return (
<form onSubmit={handleSubmit}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Cari artikel..."
/>
<button type="submit">Cari</button>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="/tentang" onClick={e => handleLink(e, "/tentang")}>
Tentang Kami
</a>
</form>
);
}
Controlled Component — Cara yang Direkomendasikan
Controlled component: nilai input dikendalikan oleh state React. React adalah “sumber kebenaran” tunggal:
import { useState } from 'react';
function FormRegistrasi() {
const [form, setForm] = useState({
nama: "",
email: "",
password: "",
peran: "pengguna",
setuju: false
});
// Handler generik untuk semua field teks
const handleUbah = (e) => {
const { name, value, type, checked } = e.target;
setForm(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log("Data form:", form);
};
return (
<form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
<input
name="nama"
value={form.nama}
onChange={handleUbah}
placeholder="Nama lengkap"
/>
<input
name="email"
type="email"
value={form.email}
onChange={handleUbah}
placeholder="Email"
/>
<input
name="password"
type="password"
value={form.password}
onChange={handleUbah}
placeholder="Password"
/>
{/* Select */}
<select name="peran" value={form.peran} onChange={handleUbah}>
<option value="pengguna">Pengguna</option>
<option value="kontributor">Kontributor</option>
<option value="admin">Admin</option>
</select>
{/* Checkbox */}
<label>
<input
name="setuju"
type="checkbox"
checked={form.setuju}
onChange={handleUbah}
/>
{" "}Saya setuju dengan syarat & ketentuan
</label>
<button type="submit" disabled={!form.setuju}>Daftar</button>
</form>
);
}
Validasi Form
import { useState } from 'react';
function FormLogin() {
const [form, setForm] = useState({ email: "", password: "" });
const [errors, setErrors] = useState({});
const [terkirim, setTerkirim] = useState(false);
const validasi = (data) => {
const err = {};
if (!data.email) {
err.email = "Email wajib diisi";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
err.email = "Format email tidak valid";
}
if (!data.password) {
err.password = "Password wajib diisi";
} else if (data.password.length < 8) {
err.password = "Password minimal 8 karakter";
}
return err;
};
const handleUbah = (e) => {
const { name, value } = e.target;
const formBaru = { ...form, [name]: value };
setForm(formBaru);
// Validasi real-time: hapus error saat sudah benar
const err = validasi(formBaru);
setErrors(prev => ({ ...prev, [name]: err[name] }));
};
const handleSubmit = (e) => {
e.preventDefault();
const err = validasi(form);
setErrors(err);
if (Object.keys(err).length === 0) {
setTerkirim(true);
console.log("Login dengan:", form.email);
}
};
if (terkirim) return <p style={{ color: "var(--color-primary)" }}>Login berhasil!</p>;
return (
<form onSubmit={handleSubmit} style={{ display: "flex", flexDirection: "column", gap: "16px", maxWidth: "320px" }}>
<div>
<input
name="email"
type="email"
value={form.email}
onChange={handleUbah}
placeholder="Email"
style={{ borderColor: errors.email ? "#FF6B6B" : "var(--color-border)" }}
/>
{errors.email && (
<p style={{ color: "#FF6B6B", fontSize: "12px", margin: "4px 0 0" }}>
{errors.email}
</p>
)}
</div>
<div>
<input
name="password"
type="password"
value={form.password}
onChange={handleUbah}
placeholder="Password"
style={{ borderColor: errors.password ? "#FF6B6B" : "var(--color-border)" }}
/>
{errors.password && (
<p style={{ color: "#FF6B6B", fontSize: "12px", margin: "4px 0 0" }}>
{errors.password}
</p>
)}
</div>
<button type="submit">Masuk</button>
</form>
);
}
Lifting State Up — Berbagi State Antar Komponen
Ketika dua komponen perlu berbagi data, pindahkan state ke komponen induk terdekat:
import { useState } from 'react';
// Komponen child: menerima nilai dan handler dari parent
function InputAngka({ label, nilai, onUbah }) {
return (
<div>
<label>{label}: </label>
<input
type="number"
value={nilai}
onChange={e => onUbah(Number(e.target.value))}
min="0"
/>
</div>
);
}
// Komponen child: hanya menampilkan data
function HasilKalkulasi({ panjang, lebar }) {
const luas = panjang * lebar;
const keliling = 2 * (panjang + lebar);
return (
<div style={{ marginTop: "16px", padding: "12px", backgroundColor: "var(--color-bg-subtle)" }}>
<p>Luas: <strong>{luas} m²</strong></p>
<p>Keliling: <strong>{keliling} m</strong></p>
</div>
);
}
// Parent: menyimpan state dan meneruskan ke kedua child
function KalkulatorPersegi() {
const [panjang, setPanjang] = useState(0);
const [lebar, setLebar] = useState(0);
return (
<div>
<h3>Kalkulator Persegi Panjang</h3>
{/* State di parent, dikirim ke child sebagai props */}
<InputAngka label="Panjang (m)" nilai={panjang} onUbah={setPanjang} />
<InputAngka label="Lebar (m)" nilai={lebar} onUbah={setLebar} />
{/* Kedua child bisa akses state yang sama */}
<HasilKalkulasi panjang={panjang} lebar={lebar} />
</div>
);
}
Uncontrolled Component dengan useRef
Kadang kamu tidak perlu state untuk setiap perubahan input — cukup ambil nilainya saat submit:
import { useRef } from 'react';
function FormUpload() {
const namaRef = useRef(null);
const fileRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
const nama = namaRef.current.value;
const file = fileRef.current.files[0];
if (!nama || !file) {
alert("Isi semua field!");
return;
}
console.log("Nama:", nama);
console.log("File:", file.name, file.size, "bytes");
// Proses upload...
};
return (
<form onSubmit={handleSubmit}>
{/* Uncontrolled: React tidak track setiap keystroke */}
<input ref={namaRef} placeholder="Nama dokumen" />
<input ref={fileRef} type="file" accept=".pdf,.doc,.docx" />
<button type="submit">Upload</button>
</form>
);
}
Kapan pakai uncontrolled? Untuk input file (
<input type="file">) karena React tidak bisa mengontrol nilainya, atau untuk form sederhana yang tidak memerlukan validasi real-time.
Pertanyaan yang Sering Diajukan
Apa perbedaan controlled dan uncontrolled component?
Controlled component: nilai input selalu mencerminkan state React — setiap perubahan memanggil setter dan React me-render ulang. Uncontrolled component: nilai disimpan di DOM itu sendiri, React hanya membaca nilainya saat dibutuhkan (biasanya via useRef). Controlled lebih direkomendasikan karena memudahkan validasi, transformasi input, dan debugging — nilai form selalu tersinkron dengan state.
Mengapa e.target.name lebih baik daripada handler terpisah untuk setiap field?
Handler generik dengan e.target.name menggunakan atribut name HTML untuk mengidentifikasi field mana yang berubah, lalu memperbarui state dengan computed property key [name]: value. Ini mengurangi duplikasi kode — tanpa teknik ini, kamu perlu handleNamaUbah, handleEmailUbah, handlePasswordUbah yang semuanya hampir identik.
Apa itu “lifting state up” dan kapan diperlukan?
Lifting state up adalah memindahkan state ke komponen induk ketika dua komponen saudara (sibling) perlu berbagi atau sinkronisasi data. Jika komponen A dan B perlu akses data yang sama, simpan di parent mereka dan kirim ke A dan B sebagai props. Ini adalah pola komunikasi sibling di React — karena data hanya mengalir ke bawah (props), komunikasi antar sibling harus melalui parent.
Bagaimana cara membuat validasi yang tidak mengganggu UX?
Strategi terbaik: (1) validasi saat blur (pengguna meninggalkan field) — tidak mengganggu saat mengetik, (2) validasi real-time hanya untuk menghapus error (ketika sudah valid), (3) validasi penuh saat submit. Hindari menampilkan error saat pengguna masih mengetik — terasa agresif. Gunakan event onBlur untuk validasi pertama kali dan onChange hanya untuk membersihkan error yang sudah ada.
Kesimpulan
| Konsep | Cara Penggunaan |
|---|---|
| Event handler | onClick={handler} — tanpa () |
| Passing argumen | onClick={() => handler(arg)} |
| Prevent default | e.preventDefault() di handler |
| Controlled input | value={state} + onChange={setter} |
| Handler generik | [e.target.name]: e.target.value |
| Validasi | Fungsi validasi() → objek error |
| Lifting state up | State di parent, kirim via props |
| Uncontrolled | useRef + baca saat submit |
Artikel sebelumnya: State dan Hooks di React — mengelola state dengan useState dan useEffect.
Langkah selanjutnya: React dengan TypeScript — cara membuat komponen React yang benar-benar type-safe.