Langsung ke konten
KamusNgoding
Menengah React 3 menit baca

Event Handling dan Form di React: Interaksi Pengguna yang Lengkap

#react #event #form #controlled-component #uncontrolled #validation #lifting-state-up #onSubmit #onChange
📚

Baca dulu sebelum ini:

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} bukan onClick={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

KonsepCara Penggunaan
Event handleronClick={handler} — tanpa ()
Passing argumenonClick={() => handler(arg)}
Prevent defaulte.preventDefault() di handler
Controlled inputvalue={state} + onChange={setter}
Handler generik[e.target.name]: e.target.value
ValidasiFungsi validasi() → objek error
Lifting state upState di parent, kirim via props
UncontrolleduseRef + 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.

Artikel Terkait