kodingup.camp

Beranda/Various Library Mastery

Struktur Form dengan React Hook Form

Modul 10 dari 134 menit bacaForm Structure Fundamentals

Isi modul

    Kenapa ini penting

    Form ada di mana-mana: login, daftar, checkout, tambah tugas, edit profil. Dengan React biasa, setiap input butuh useState, handler onChange, logika validasi, pesan error, status "sedang mengirim", dan reset setelah sukses. Form dengan 10 input bisa menghasilkan puluhan baris kode berulang, dan karena setiap ketikan mengubah state, seluruh form re-render di setiap huruf.

    React Hook Form (RHF) menyelesaikan ini dengan pendekatan berbeda: ia membiarkan browser yang menyimpan nilai input (uncontrolled) dan hanya "membaca" nilai saat dibutuhkan. Hasilnya: kode lebih ringkas, re-render jauh lebih sedikit, dan validasi tetap lengkap. Versi yang dipakai: react-hook-form 7.90.0.

    Konsep inti

    Controlled vs uncontrolled

    Analogi: controlled itu seperti guru yang berdiri di samping murid dan mencatat setiap huruf yang ditulis murid di buku catatannya sendiri. Uncontrolled itu seperti guru yang membiarkan murid menulis, lalu baru membaca lembar jawaban saat dikumpulkan. Yang kedua jauh lebih hemat tenaga, dan itulah cara kerja RHF.

    useForm

    const {
      register,      // menghubungkan input ke form
      handleSubmit,  // membungkus fungsi submit + menjalankan validasi
      formState,     // status form: errors, isSubmitting, isDirty, isValid, ...
      reset,         // mengembalikan form ke nilai awal
      watch,         // memantau nilai input (memicu render)
      setValue,      // mengisi nilai secara program
      setError,      // memasang error manual (misalnya dari server)
      control,       // dipakai untuk komponen controlled (Controller)
    } = useForm({
      defaultValues: { title: "", priority: "sedang" },
      mode: "onTouched", // kapan validasi berjalan
    });
    

    Pilihan mode:

    mode Validasi berjalan saat...
    onSubmit (default) Tombol submit ditekan
    onBlur Input kehilangan fokus
    onChange Setiap ketikan (paling banyak render)
    onTouched Pertama kali saat blur, lalu setiap perubahan
    all Blur dan change

    register

    register("nama") mengembalikan { name, onChange, onBlur, ref } yang kamu sebar ke input:

    <input {...register("title", { required: "Judul wajib diisi", minLength: { value: 3, message: "Minimal 3 karakter" } })} />
    

    Aturan validasi bawaan: required, minLength, maxLength, min, max, pattern, validate (fungsi kustom). Ada juga opsi transformasi: valueAsNumber: true (input angka menjadi number, input kosong menjadi NaN) dan valueAsDate: true.

    handleSubmit

    <form onSubmit={handleSubmit(onValid, onInvalid)}>
    

    handleSubmit mencegah reload halaman (preventDefault), menjalankan validasi, lalu memanggil onValid(data) kalau valid atau onInvalid(errors) kalau tidak. Kalau onValid adalah fungsi async, formState.isSubmitting bernilai true sampai Promise-nya selesai, cocok untuk menonaktifkan tombol.

    formState

    Properti Arti
    errors Object error per field, misalnya errors.title.message
    isSubmitting Sedang menjalankan onValid
    isSubmitSuccessful Submit terakhir berhasil
    isDirty Ada nilai yang berbeda dari defaultValues
    isValid Semua field valid
    touchedFields, dirtyFields Field mana yang sudah disentuh / diubah

    RHF memakai Proxy: ia hanya memantau properti formState yang benar-benar kamu baca. Karena itu, destructure properti yang kamu butuhkan, misalnya const { errors, isSubmitting } = formState.

    Komponen controlled dengan Controller

    Beberapa komponen UI (date picker, select kustom, checkbox dari library UI) tidak bekerja dengan ref input biasa; mereka butuh value dan onChange. Untuk itu ada Controller, yang akan kita pakai di modul shadcn/ui.

    Kapan tetap memakai useState biasa?

    React Hook Form bukan kewajiban untuk setiap input. Kolom pencarian tunggal yang hasilnya langsung memfilter daftar, atau toggle sederhana, lebih wajar memakai useState atau store Zustand, karena memang perlu bereaksi di setiap ketikan. RHF bersinar ketika ada form yang dikirim: beberapa field, aturan validasi, status mengirim, dan reset.

    Contoh nyata: halaman checkout toko online punya nama, alamat, kota, kode pos, nomor HP, dan catatan. Dengan RHF, mengetik alamat panjang tidak membuat ringkasan belanja di sisi kanan ikut dirender ulang, dan semua pesan error bisa muncul sekaligus saat tombol "Bayar" ditekan.

    Contoh kode

    // src/features/todos/TaskForm.jsx, form tambah tugas dengan validasi bawaan RHF
    import { useForm } from "react-hook-form";
    
    const tunggu = (ms) => new Promise((r) => setTimeout(r, ms));
    
    export default function TaskForm() {
      const {
        register,
        handleSubmit,
        reset,
        watch,
        formState: { errors, isSubmitting, isDirty },
      } = useForm({
        defaultValues: { title: "", priority: "sedang", estimasi: 1, catatan: "" },
        mode: "onTouched",
      });
    
      const judul = watch("title"); // memantau satu field untuk penghitung karakter
    
      async function onValid(data) {
        await tunggu(800); // pura-pura kirim ke server
        console.log("Data dikirim:", data);
        reset(); // kosongkan form setelah sukses
      }
    
      return (
        <form onSubmit={handleSubmit(onValid)} className="mx-auto max-w-md space-y-4 p-6" noValidate>
          <div>
            <label htmlFor="title" className="block text-sm font-medium">Judul tugas</label>
            <input
              id="title"
              className="w-full rounded border px-3 py-2"
              aria-invalid={errors.title ? "true" : "false"}
              {...register("title", {
                required: "Judul wajib diisi",
                minLength: { value: 3, message: "Judul minimal 3 karakter" },
                maxLength: { value: 80, message: "Judul maksimal 80 karakter" },
              })}
            />
            <p className="text-xs text-slate-500">{judul.length}/80</p>
            {errors.title && <p className="text-sm text-red-600">{errors.title.message}</p>}
          </div>
    
          <div>
            <label htmlFor="priority" className="block text-sm font-medium">Prioritas</label>
            <select id="priority" className="w-full rounded border px-3 py-2" {...register("priority")}>
              <option value="rendah">Rendah</option>
              <option value="sedang">Sedang</option>
              <option value="tinggi">Tinggi</option>
            </select>
          </div>
    
          <div>
            <label htmlFor="estimasi" className="block text-sm font-medium">Estimasi (jam)</label>
            <input
              id="estimasi"
              type="number"
              className="w-full rounded border px-3 py-2"
              {...register("estimasi", {
                valueAsNumber: true,
                min: { value: 1, message: "Minimal 1 jam" },
                max: { value: 40, message: "Maksimal 40 jam" },
              })}
            />
            {errors.estimasi && <p className="text-sm text-red-600">{errors.estimasi.message}</p>}
          </div>
    
          <div>
            <label htmlFor="catatan" className="block text-sm font-medium">Catatan</label>
            <textarea id="catatan" className="w-full rounded border px-3 py-2" {...register("catatan")} />
          </div>
    
          <button
            type="submit"
            disabled={isSubmitting || !isDirty}
            className="rounded bg-slate-900 px-4 py-2 text-white disabled:opacity-50"
          >
            {isSubmitting ? "Menyimpan..." : "Simpan tugas"}
          </button>
        </form>
      );
    }
    // → Submit dengan judul "ab": muncul "Judul minimal 3 karakter".
    // → Submit valid: console mencetak
    //   Data dikirim: { title: "Belajar RHF", priority: "tinggi", estimasi: 3, catatan: "" }
    //   perhatikan estimasi berupa number (bukan "3") karena valueAsNumber.
    

    Atribut noValidate mematikan validasi bawaan browser (gelembung "Please fill out this field") supaya pesan dari RHF yang tampil dan konsisten di semua browser.

    Kesalahan umum

    Latihan

    1. Mudah, Buat form login dengan email dan password. Keduanya wajib; tampilkan pesan error di bawah input.
    2. Sedang, Tambahkan aturan pattern untuk email dan validate kustom untuk password: harus mengandung minimal satu angka.
    3. Menantang, Buat form "Edit tugas" yang menerima prop task. Isi form dengan data tugas itu (pakai defaultValues dan reset saat prop berubah), dan tombol "Simpan" hanya aktif kalau ada perubahan (isDirty).
    Lihat solusi
    // 1 & 2. Form login
    import { useForm } from "react-hook-form";
    
    export function LoginForm() {
      const { register, handleSubmit, formState: { errors } } = useForm({
        defaultValues: { email: "", password: "" },
      });
    
      return (
        <form onSubmit={handleSubmit((data) => console.log(data))} noValidate>
          <input
            placeholder="Email"
            {...register("email", {
              required: "Email wajib diisi",
              pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Format email tidak valid" },
            })}
          />
          {errors.email && <p>{errors.email.message}</p>}
    
          <input
            type="password"
            placeholder="Password"
            {...register("password", {
              required: "Password wajib diisi",
              validate: (v) => /\d/.test(v) || "Password harus mengandung angka",
            })}
          />
          {errors.password && <p>{errors.password.message}</p>}
    
          <button type="submit">Masuk</button>
        </form>
      );
    }
    // → password "rahasia" → "Password harus mengandung angka"
    
    // 3. Form edit
    import { useEffect } from "react";
    import { useForm } from "react-hook-form";
    
    export function EditTaskForm({ task, onSave }) {
      const { register, handleSubmit, reset, formState: { isDirty, isSubmitting } } = useForm({
        defaultValues: { title: task.title, completed: task.completed },
      });
    
      // kalau task yang diedit berganti, isi ulang form dengan data baru
      useEffect(() => {
        reset({ title: task.title, completed: task.completed });
      }, [task, reset]);
    
      return (
        <form onSubmit={handleSubmit(onSave)}>
          <input {...register("title", { required: "Judul wajib diisi" })} />
          <label>
            <input type="checkbox" {...register("completed")} /> Selesai
          </label>
          <button type="submit" disabled={!isDirty || isSubmitting}>Simpan</button>
        </form>
      );
    }
    

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.