Episode ini membahas penanganan form di komponen React, validasi client-side dengan React Hook Form dan Zod, validasi server-side serta pengiriman ke API dengan Server Actions, dan aksesibilitas form dengan feedback UX yang baik.

Form adalah gerbang pengguna masuk ke aplikasi: pendaftaran, login, pencarian, hingga pembayaran. Form yang buruk — validasi membingungkan, error tidak jelas, input hilang saat gagal — membuat pengguna frustrasi dan meninggalkan aplikasi.
Episode 10 membahas penanganan form di komponen React, validasi client-side dengan React Hook Form dan Zod, validasi server-side dan pengiriman data dengan Server Actions, serta aksesibilitas form dan feedback UX.
Tanpa library, form React dikelola dengan state per input. Untuk form kecil, pendekatan ini cukup:
"use client"
import { useState } from "react"
export default function ContactForm() {
const [email, setEmail] = useState("")
return (
<form onSubmit={(e) => e.preventDefault()}>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</form>
)
}Input di atas terkontrol oleh state email. Untuk form dengan banyak field, validasi kompleks, dan pengalaman error yang halus, React Hook Form menawarkan kontrol lebih baik.
React Hook Form mengelola registrasi input dan menghindari re-render berlebihan. Kombinasikan dengan Zod lewat zodResolver untuk skema validasi yang jelas:
"use client"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
const schema = z.object({
email: z.string().email("Format email tidak valid"),
nama: z.string().min(3, "Nama minimal 3 karakter"),
})
type FormValues = z.infer<typeof schema>
export default function RegisterForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
})
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} type="email" />
{errors.email && <p>{errors.email.message}</p>}
<button type="submit">Daftar</button>
</form>
)
}Skema Zod di atas memvalidasi format email dan panjang nama. errors.email.message menampilkan pesan error di bawah input yang gagal validasi — feedback langsung tanpa submit.
Validasi client-side hanya untuk UX; validasi server adalah garis pertahanan sebenarnya. Pengguna bisa mem-bypass JavaScript dengan mudah, jadi server wajib memvalidasi ulang semua input. Kedua lapis harus memakai aturan yang sama — dan Zod memungkinkan skema dipakai bersama di klien dan server.
Server Actions memungkinkan fungsi di server dipanggil langsung dari form tanpa menulis endpoint API. Tulis fungsi dengan direktif "use server" dan validasi dengan skema yang sama:
"use server"
import { z } from "zod"
const schema = z.object({
email: z.string().email(),
})
export async function daftarPengguna(prevState, formData) {
const parsed = schema.safeParse({
email: formData.get("email"),
})
if (!parsed.success) {
return { error: "Data tidak valid" }
}
return { success: "Pendaftaran berhasil" }
}schema.safeParse(formData) memvalidasi data dari form tanpa melempar exception — hasil sukses atau gagal dibaca dari objek kembalian. Server Action mengembalikan state yang ditampilkan di form.
Di komponen klien, pakai hook useActionState untuk mengikat Server Action dengan state form:
"use client"
import { useActionState } from "react"
import { daftarPengguna } from "./actions"
export default function Form() {
const [state, formAction] = useActionState(daftarPengguna, null)
return (
<form action={formAction}>
<input name="email" type="email" />
{state && <p>{state.error ?? state.success}</p>}
<button type="submit">Kirim</button>
</form>
)
}useActionState(daftarPengguna, null) menjalankan Server Action dan menampilkan hasilnya. Form tetap bisa berfungsi tanpa JavaScript — pola progressive enhancement yang disukai mesin pencari.
Form yang aksesibel dimulai dari HTML semantik: label terhubung dengan input lewat htmlFor, pesan error ditautkan dengan atribut aria-describedby, dan field wajib ditandai required. Screen reader memakai hubungan ini untuk membacakan error dengan benar. UX feedback yang baik: tampilkan error tepat di bawah field terkait, jangan hapus input pengguna saat submit gagal, dan beri indikator loading saat pengiriman berlangsung.
Inti yang harus dibawa pulang:
Di episode 11 selanjutnya kita akan membahas internationalization (i18n) — dukungan multi-bahasa dengan next-intl, routing lokalized dan terjemahan konten, serta SEO dan metadata untuk aplikasi multi-bahasa. Aplikasi kalian siap menjangkau pengguna global.