Episode ini membedah form di Remix: komponen Form untuk submission, validasi server-side dengan feedback di client, rendering error per field, serta progressive enhancement yang membuat form tetap berfungsi tanpa JavaScript.

Di episode 5 kalian melihat action menangani data dari form. Episode 7 menggali sisi itu sampai tuntas, karena form adalah bagian web yang paling sering diremehkan — dan di Remix, form menjadi salah satu alasan utama memilih framework ini.
Model form Remix memakai ulang perilaku browser: form HTML biasa mengirim data ke server, dan server mengembalikan respons. Remix menyempurnakannya dengan memproses form di action, me-refresh loader otomatis setelah selesai, dan menampilkan error langsung di UI — semua tanpa JavaScript. Saat JavaScript aktif, pengalaman meningkat: navigasi halus, validasi cepat, dan state yang dipertahankan.
Episode 7 akan membangun form lengkap: submission, validasi server-side, error per field, lalu lapisan progressive enhancement dengan useFetcher.
Komponen Form dari @remix-run/react menggantikan tag form biasa. Pada dasarnya perilakunya identik, tetapi saat JavaScript berjalan, submission ditangani tanpa reload halaman penuh — action tetap dieksekusi, loader di-refresh, dan URL diperbarui.
import { Form } from "@remix-run/react";
export default function FormKontak() {
return (
<Form method="post">
<label>
Nama
<input type="text" name="nama" />
</label>
<button type="submit">Kirim</button>
</Form>
);
}Attribute method="post" pada Form mengarahkan submission ke action route. Setiap input yang bernama ikut terkirim sebagai FormData ke action.
Di action, data form dibaca dengan request.formData() dan diakses lewat method get atau getAll:
export async function action({ request }) {
const formData = await request.formData();
const nama = formData.get("nama");
const hobi = formData.getAll("hobi");
return { ok: Boolean(nama) };
}formData.getAll("hobi") mengembalikan semua nilai input bernama hobi — berguna untuk checkbox atau array. Jangan pernah mengirim data yang tidak dibutuhkan; hanya field yang bernama yang terkirim.
Aturan emasnya: validasi di client hanya untuk kenyamanan, validasi di server untuk kebenaran. Remix menempatkan validasi di action karena data dari client tidak bisa dipercaya. Validasi manual cukup untuk form kecil; untuk form kompleks, gunakan library seperti Zod.
export async function action({ request }) {
const formData = await request.formData();
const email = formData.get("email");
const errors = {};
if (!email || !String(email).includes("@")) {
errors.email = "Format email tidak valid";
}
if (Object.keys(errors).length > 0) {
return { errors, fieldValue: { email } };
}
await simpanKeDatabase(email);
return redirect("/terima-kasih");
}Objek errors mengikuti nama field; action mengembalikannya agar UI bisa menampilkan. Jika ada error, action mengembalikan nilai field juga supaya input tidak kosong saat halaman di-render ulang.
Untuk form yang banyak field, Zod memberi schema typing dan pesan error terstruktur. Schema Zod di-parse terhadap Object.fromEntries(formData) di dalam action — ini pola yang umum di production dan akan muncul lagi di episode 18.
Komponen membaca error hasil action dengan useActionData, lalu menampilkannya tepat di bawah input terkait. Karena useActionData kembali kosong sebelum action dijalankan, UI pertama kali bersih dan error muncul hanya setelah submission.
import { useActionData, Form } from "@remix-run/react";
export default function FormEmail() {
const data = useActionData();
return (
<Form method="post">
<input type="email" name="email" defaultValue={data?.fieldValue?.email} />
{data?.errors?.email ? <p className="error">{data.errors.email}</p> : null}
<button type="submit">Kirim</button>
</Form>
);
}useActionData mengembalikan hasil action terakhir pada route ini. Pola defaultValue plus errors menjaga nilai input tetap ada setelah halaman di-render ulang karena error.
Untuk pengguna screen reader, hubungkan error dengan input memakai aria-describedby. Error yang ditautkan dengan benar dibacakan saat input difokuskan — detail kecil yang membedakan aplikasi yang baik dari yang sekadar bekerja.
Uji form kalian dengan menonaktifkan JavaScript di browser. Form tetap terkirim, action tetap berjalan, error tetap muncul — karena semua itu adalah perilaku native browser. Inilah progressive enhancement: form bekerja sebelum JavaScript, dan meningkat setelahnya.
Saat form perlu dikirim tanpa berpindah halaman — misalnya tombol suka, filter, atau panel login di sidebar — gunakan useFetcher. Fetcher mengirim FormData ke action dari mana saja, tanpa navigasi.
import { useFetcher } from "@remix-run/react";
export default function TombolSuka({ postId }) {
const fetcher = useFetcher();
return (
<fetcher.Form method="post">
<input type="hidden" name="postId" value={postId} />
<button type="submit">Suka</button>
</fetcher.Form>
);
}fetcher.Form mengirim ke action tanpa berpindah halaman — data yang sama diterima action, dan loader route bisa membaca state baru setelahnya. Detail status submission tersedia di fetcher.state.
Episode 7 melengkapi kemampuan form kalian: Form dan FormData untuk submission, validasi server-side sebagai sumber kebenaran, rendering error per field yang aksesibel, dan progressive enhancement lewat useFetcher. Form kalian kini bekerja di semua kondisi — dengan atau tanpa JavaScript.
Inti yang harus dibawa pulang:
Di episode 8 selanjutnya kita akan membahas configuration dan environment — peran vite.config.ts dan environment variables, pemilihan build target, adapter dan runtime, pengelolaan aset di public, serta optimasi build untuk production. Form sudah berfungsi; sekarang kita rapikan rumahnya.