Lompat ke konten
Kembali ke modul

React: Membangun Antarmuka Pengguna · 9/9

Form di React

Input terkontrol, umpan balik validasi, dan submit tanpa menghilangkan pekerjaan pengguna.

Baca 30 menit

Setelah pelajaran ini kamu bisa

  • Menjelaskan bedanya input terkontrol dan tidak terkontrol
  • Memilih di antaranya dengan sadar
  • Menangani submit, state pending, dan error
  • Memakai `useActionState` dari React 19 dengan sebuah Server Action

Ada dua cara memiliki sebuah field form. Tidak ada yang salah, dan sebagian besar tutorial hanya menampilkan satu.

TerkontrolTidak terkontrol
Nilainya tinggal diState ReactElemen DOM-nya
Re-render setiap ketikanYaTidak
Membaca nilainyaKapan saja, dari stateSaat submit, lewat FormData
Dibutuhkan untukValidasi langsung, format, field yang saling bergantungSubmit-dan-kirim biasa
Kode yang dibutuhkanLebih banyakHampir tidak ada
tsx
// Controlled: state is the single source of truth.
function ControlledSearch() {
  const [query, setQuery] = useState("");

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      {/* Only possible because the value is in state: */}
      <p>{query.length} characters</p>
      <button disabled={query.length < 3}>Search</button>
    </>
  );
}

// Uncontrolled: the DOM keeps the value, and we read it on submit.
function UncontrolledSearch({ onSearch }) {
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        onSearch(new FormData(e.currentTarget).get("query"));
      }}
    >
      <input name="query" defaultValue="" />
      <button type="submit">Search</button>
    </form>
  );
}
Perhatikan defaultValue, bukan value, untuk yang tidak terkontrol — memakai value tanpa onChange menghasilkan field yang tidak bisa diketik penggunanya, dan itu kesalahan pertama yang sangat umum.

Submit punya tiga state

Setiap form yang bicara ke server harus menangani state pending dan error, bukan cuma sukses. Ini discriminated union dari modul TypeScript, diterapkan pada UI yang paling umum ada.

Coba sendiri

State machine yang benar-benar dibutuhkan sebuah form, sebagai reducer. Perhatikan transisi mana yang memang tidak mungkin.

Hasil

Tekan Jalankan untuk melihat hasilnya.

Ini jalan di browser kamu, di dalam sandbox. Apa pun yang kamu tulis di sini tidak bisa merusak situs.

React 19: form action

React 19 menangani sebagian besar pembukuan itu untukmu. Sebuah form bisa menerima function sebagai action-nya, dan useActionState melacak nilai yang dikembalikan serta penanda pending-nya.

tsx
"use client";
import { useActionState } from "react";
import { signInAction } from "./actions";

export function SignInForm() {
  // [ whatever the action returned, the function to pass to <form>, is it pending ]
  const [result, formAction, pending] = useActionState(signInAction, undefined);

  return (
    <form action={formAction}>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" required autoComplete="username" />

      <label htmlFor="password">Password</label>
      <input id="password" name="password" type="password" required
             autoComplete="current-password" />

      {result?.error && (
        <p role="alert" aria-live="polite">{messages[result.error]}</p>
      )}

      <button type="submit" disabled={pending}>
        {pending ? "Signing in..." : "Sign in"}
      </button>
    </form>
  );
}
Ini features/auth/auth-form.tsx dari proyek ini, diringkas. Baca yang aslinya — bentuknya sama dengan pesan yang dilokalkan dan aria-invalid.
  • `pending` sudah gratis. Tidak ada useState untuk melacaknya, dan tidak mungkin lupa mematikannya di jalur error.
  • Action-nya menerima `FormData`. Input tidak terkontrol, jadi tidak perlu state per field.
  • Ia bekerja sebelum hydration. <form action> tetap ter-submit meski JavaScript belum termuat — progressive enhancement secara default.
  • Submit ganda sudah ditangani. React mematikan pengiriman ulang selama pending.

Tugas praktik

Di proyek ini, buka features/auth/auth-form.tsx dan petakan setiap bagiannya ke pelajaran ini: input mana yang tidak terkontrol, pending datang dari mana, bagaimana error-nya di-render secara aksesibel, dan kenapa locale dikirim lewat input tersembunyi. Lalu rusak dengan sengaja — hapus disabled={pending} lalu submit dua kali dengan cepat. Perhatikan apa yang dilakukan server terhadapnya.

Hasil yang diharapkan

Pendaftaran kedua ditolak sebagai nama yang sudah dipakai — constraint UNIQUE-nya adalah pengaman sebenarnya.