Artikel ini disponsori oleh SurveyJS Ada model mental yang dibagikan oleh sebagian besar pengembang React tanpa pernah mendiskusikannya secara terbuka. Bentuk-bentuk itu selalu dianggap sebagai komponen. Ini berarti tumpukan seperti:
React Hook Form untuk negara bagian lokal (render ulang minimal, registrasi lapangan ergonomis, interaksi penting). Zod untuk validasi (kebenaran input, validasi batas, penguraian aman tipe). React Query untuk backend: pengiriman, percobaan ulang, caching, sinkronisasi server, dan sebagainya.
Dan untuk sebagian besar formulir — layar login Anda, halaman pengaturan Anda, modal CRUD Anda — ini bekerja dengan sangat baik. Setiap bagian melakukan tugasnya, disusun dengan rapi, dan Anda dapat beralih ke bagian aplikasi yang benar-benar membedakan produk Anda. Namun sesekali, formulir mulai mengumpulkan hal-hal seperti aturan visibilitas yang bergantung pada jawaban sebelumnya, atau nilai turunan yang mengalir melalui tiga bidang. Bahkan mungkin seluruh halaman yang harus dilewati atau ditampilkan berdasarkan total berjalan. Anda menangani kondisi pertama dengan useWatch dan cabang inline, dan itu tidak masalah. Lalu yang lain. Kemudian Anda menggunakan superRefine untuk menyandikan aturan lintas bidang yang tidak dapat diungkapkan oleh skema Zod Anda dengan cara biasa. Kemudian, navigasi langkah mulai membocorkan logika bisnis. Pada titik tertentu, Anda melihat apa yang telah Anda buat dan menyadari bahwa formulir tersebut bukan lagi UI. Ini lebih merupakan proses pengambilan keputusan, dan pohon komponen adalah tempat Anda menyimpannya. Di sinilah menurut saya model mental untuk formulir di React rusak, dan ini sebenarnya bukan salah siapa pun. Tumpukan RHF + Zod sangat bagus dalam hal desainnya. Masalahnya adalah kita cenderung terus menggunakannya melewati titik di mana abstraksinya sesuai dengan permasalahan karena alternatifnya memerlukan cara berpikir yang berbeda tentang bentuk secara keseluruhan. Artikel ini tentang alternatif itu. Untuk menunjukkan hal ini, kami akan membuat formulir multi-langkah yang sama persis sebanyak dua kali:
Dengan React Hook Form + Zod yang dihubungkan ke React Query untuk pengiriman, Dengan SurveyJS, yang memperlakukan formulir sebagai data — skema JSON sederhana — dan bukan pohon komponen.
Persyaratan yang sama, logika kondisional yang sama, panggilan API yang sama di akhir. Kemudian kita akan memetakan dengan tepat apa yang berpindah dan apa yang tersisa, serta memaparkan cara praktis untuk memutuskan model mana yang harus Anda gunakan, dan kapan. Formulir yang kami buat:
Formulir ini akan menggunakan alur 4 langkah: Langkah 1: Detail
Nama depan (wajib), Email (wajib, format valid).
Langkah 2: Pesan
Harga satuan, Kuantitas, tarif pajak, Berasal: Subtotal, Pajak, Jumlah.
Langkah 3: Akun & Umpan Balik
Apakah Anda punya akun? (Ya/Tidak) Jika Ya → nama pengguna + kata sandi, keduanya diperlukan. Jika Tidak → email sudah dikumpulkan di langkah 1.
Peringkat kepuasan (1–5) Jika ≥ 4 → tanyakan “Apa yang kamu suka?” Jika ≤ 2 → tanyakan “Apa yang bisa kita tingkatkan?”
Langkah 4: Tinjau
Hanya muncul jika total >= 100 Penyerahan terakhir.
Ini tidak ekstrem. Tapi itu cukup untuk mengungkap perbedaan arsitektural. Bagian 1: Berbasis Komponen (React Hook Form + Zod) Instalasi npm instal react-hook-form zod @hookform/resolver @tanstack/react-query
Skema Zod Mari kita mulai dengan skema Zod, karena biasanya di sinilah bentuk terbentuk. Untuk dua langkah pertama — detail pribadi dan masukan pesanan — semuanya mudah: string yang diperlukan, angka dengan minimum, dan enum. Bagian yang menarik dimulai ketika Anda mencoba mengungkapkan aturan bersyarat.
impor { z } dari "zod";
ekspor const formSchema = z.object({ firstName: z.string().min(1, "Wajib"), email: z.string().email("Email tidak valid"), harga: z.number().min(0), jumlah: z.number().min(1), taxRate: z.number(), hasAccount: z.enum(["Yes", "No"]), nama pengguna: z.string().optional(), kata sandi: z.string().optional(), kepuasan: z.number().min(1).max(5), positifUmpan Balik: z.string().optional(), perbaikanUmpan Balik: z.string().optional(),}).superRefine((data, ctx) => { if (data.hasAccount === "Yes") { if (!data.username) { ctx.addIssue({ kode: "custom", jalur: ["nama pengguna"], pesan: "Wajib" } } if (!data.password || data.password.length < 6) { ctx.addIssue({ kode: "custom", path: ["password"], pesan: "Minimal 6 karakter" });
if (data.satisfaction >= 4 && !data.patientFeedback) { ctx.addIssue({ code: "custom", path: ["positifFeedback"], pesan: "Silakan bagikan apa yang Anda suka" }); }
if (data.kepuasan <= 2 && !data.improvementFeedback) { ctx.addIssue({ kode: "custom", jalur:["improvementFeedback"], pesan: "Tolong beri tahu kami apa yang perlu ditingkatkan" }); }});
tipe ekspor FormData = z.infer
Perhatikan bahwa nama pengguna dan kata sandi diketik sebagai opsional() meskipun diperlukan secara kondisional karena skema tingkat tipe Zod menjelaskan bentuk objek, bukan aturan yang mengatur kapan bidang itu penting. Persyaratan kondisional harus ada di dalam superRefine, yang berjalan setelah bentuk divalidasi dan memiliki akses ke objek penuh. Pemisahan itu bukanlah sebuah cacat; itulah tujuan alat ini dirancang: superRefine adalah tempat logika lintas bidang digunakan ketika logika tersebut tidak dapat diekspresikan dalam struktur skema itu sendiri. Yang juga penting di sini adalah apa yang tidak diungkapkan oleh skema ini. Ia tidak memiliki konsep halaman, tidak ada konsep bidang mana yang terlihat pada titik mana, dan tidak ada konsep navigasi. Semua itu akan tinggal di tempat lain. Komponen Formulir
import { useForm, useWatch } dari "react-hook-form";import { zodResolver } dari "@hookform/resolvers/zod";import { useMutation } dari "@tanstack/react-query";import { useState, useMemo } dari "react";import { formSchema, ketik FormData } dari "./schema";
const LANGKAH = ["detail", "pesanan", "akun", "ulasan"];
ketik OrderPayload = FormData & { subtotal: nomor; pajak: nomor; jumlah: angka };
fungsi ekspor RHFMultiStepForm() { const [langkah, setStep] = useState(0);
const mutasi = useMutation({ mutasiFn: async (muatan: OrderPayload) => { const res = menunggu pengambilan("/api/pesanan", { metode: "POSTING", header: { "Tipe Konten": "application/json" }, isi: JSON.stringify(muatan), }); if (!res.ok) throw new Error("Gagal mengirimkan"); kembalikan res.json(); }, });
const { register, control, handleSubmit, formState: { error }, } = useForm
return (
);}Lihat Pen SurveyJS-03-RHF [bercabang] pada kepunahan keenam. Ada cukup banyak hal yang terjadi di sini, dan ada baiknya kita memperlambat waktu untuk memperhatikan di mana akhirnya.
Nilai turunan — subtotal, pajak, total — dihitung dalam komponen melalui useWatch dan useMemo karena nilai tersebut bergantung pada nilai bidang aktif dan tidak ada tempat alami lain untuk nilai tersebut. Aturan visibilitas untuk nama pengguna, kata sandi, umpan balik positif, dan umpan balik perbaikan ada di BEJ sebagai kondisi inline. Logika lompatan langkah — halaman tinjauan hanya muncul ketika total >= 100 — disematkan ke dalam variabel showSubmit dan kondisi render pada langkah 3. Navigasi itu sendiri hanyalah penghitung useState yang kami tingkatkan secara manual. React Query menangani percobaan ulang, caching, dan pembatalan. Formulir hanya memanggil mutasi.mutate dengan data yang divalidasi.
Semua ini tidak salah. Ini masih merupakan React yang idiomatis, dan komponennya cukup berperforma berkat cara render ulang isolat RHF. Namun jika Anda menyerahkan ini kepada seseorang yang belum menulisnya dan meminta mereka menjelaskan dalam kondisi apa halaman ulasan muncul, mereka harus menelusuri showSubmit, kondisi render langkah 3, dan logika tombol navigasi — tiga tempat terpisah — untuk merekonstruksi aturan yang bisa dinyatakan dalam satu baris. Formulirnya berfungsi, ya, tetapi perilakunya tidak benar-benar dapat diperiksa sebagai suatu sistem. Itu harus dijalankan secara mental. Lebih penting lagi, mengubahnya memerlukan keterlibatan teknik. Bahkan perubahan kecil, seperti menyesuaikan kapan langkah peninjauan muncul, berarti mengedit komponen, memperbarui validasi, membuka permintaan penarikan, menunggu peninjauan, dan menerapkan lagi. Bagian 2: Berbasis Skema (SurveyJS) Sekarang mari kita buat alur yang sama menggunakan skema. Instalasi npm instal survei-inti survei-react-ui @tanstack/react-query
survey-coreMesin runtime platform-independen berlisensi MIT yang mendukung rendering formulir SurveyJS — bagian yang menjadi perhatian kami di sini. Dibutuhkan skema JSON, membangun model internal darinya, dan menangani segala sesuatu yang seharusnya ada di komponen React Anda: mengevaluasi ekspresi visibilitas, menghitung nilai turunan, mengelola status halaman, melacak validasi, dan memutuskan apa arti “lengkap” mengingat halaman mana yang sebenarnya ditampilkan.
survey-react-ui Lapisan UI/render yang menghubungkan model tersebut ke React. Ini pada dasarnya adalah komponen
Bersama-sama, keduanya memberi Anda runtime formulir multi-halaman yang berfungsi penuh tanpa menulis satu baris pun alur kontrol. Format skemanya sendiri, seperti disebutkan sebelumnya, hanyalah JSON — tanpa DSL atau hak milik apa pun. Anda dapat memasukkannya ke dalam baris, mengimpornya dari file, mengambilnya dari API, atau menyimpannya di kolom database dan menghidrasinya saat runtime. Bentuknya Sama, Seperti Data Berikut bentuk yang sama, kali ini dinyatakan sebagai objek JSON. Skema ini mendefinisikan segalanya: struktur, validasi, aturan visibilitas, penghitungan turunan, navigasi halaman — dan menyerahkannya ke Model yang mengevaluasinya saat runtime. Berikut tampilan selengkapnya:
ekspor const surveySchema = { title: "Order Flow", showProgressBar: "top", halaman: [ { name: "detail", elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true, validators: [{ type: "email", text: "Invalid email" }] } ] }, { name: "order", elemen: [ { ketik: "teks", nama: "harga", tipe input: "angka", nilai default: 0 }, { ketik: "teks", nama: "kuantitas", tipe input: "angka", nilai default: 1 }, { ketik: "dropdown",nama: "taxRate", defaultValue: 0,1, pilihan: [ { nilai: 0,05, teks: "5%" }, { nilai: 0,1, teks: "10%" }, { nilai: 0,15, teks: "15%" } ] }, { ketik: "ekspresi", nama: "subtotal", ekspresi: "{price} {quantity}" }, { ketik: "ekspresi", nama: "pajak", ekspresi: "{subtotal} {taxRate}" }, { ketik: "ekspresi", nama: "total", ekspresi: "{subtotal} + {pajak}" } ] }, { nama: "akun", elemen: [ { ketik: "radiogroup", nama: "hasAccount", pilihan: ["Ya", "Tidak"] }, { ketik: "teks", nama: "nama pengguna", terlihatJika: "{hasAccount} = 'Ya'", isRequired: true }, { ketik: "teks", nama: "kata sandi", inputType: "kata sandi", visibelIf: "{hasAccount} = 'Ya'", isRequired: true, validator: [{ ketik: "teks", minLength: 6, teks: "Minimal 6 karakter" }] }, { ketik: "rating", nama: "kepuasan", rateMin: 1, rateMax: 5 }, { ketik: "komentar", nama: "umpan balik positif", visibelIf: "{kepuasan} >= 4" }, { ketik: "komentar", nama: "umpan balik perbaikan", visibelIf: "{kepuasan} <= 2" } ] }, { nama: "ulasan", visibelIf: "{total} >= 100", elemen: [] } ]};
Bandingkan ini dengan versi RHF sejenak.
Blok superRefine yang memerlukan nama pengguna dan kata sandi bersyarat telah hilang. visibleIf: "{hasAccount} = 'Yes'" dikombinasikan dengan isRequired: true menangani kedua masalah secara bersamaan, di lapangan itu sendiri, di tempat yang Anda harapkan akan menemukannya. Rantai useWatch + useMemo yang menghitung subtotal, pajak, dan total digantikan oleh tiga bidang ekspresi yang saling mereferensikan berdasarkan nama. Kondisi halaman review, yang dalam versi RHF hanya dapat direkonstruksi dengan menelusuri melalui showSubmit, cabang render langkah 3. Dan yang terakhir, logika tombol nav adalah satu properti VisibleIf pada objek halaman.
Logika yang sama juga ada. Hanya saja skema tersebut memberinya tempat tinggal yang terlihat secara terpisah, bukan tersebar ke seluruh komponen. Perhatikan juga bahwa skema menggunakan tipe: 'ekspresi' untuk subtotal, pajak, dan total. Ekspresi bersifat baca-saja dan digunakan terutama untuk menampilkan nilai terhitung. SurveyJS juga mendukung tipe: 'html' untuk konten statis, tetapi untuk nilai terhitung, ekspresi adalah pilihan yang tepat. Sekarang untuk sisi React. Rendering dan Penyerahan Sangat sederhana. Hubungkan onComplete ke API Anda dengan cara yang sama — melalui useMutation atau pengambilan biasa:
import { useState, useEffect, useRef } dari "react";import { useMutation } dari "@tanstack/react-query";import { Model } dari "survey-core";import { Survey } dari "survey-react-ui";import "survey-core/survey-core.css";
fungsi ekspor SurveyForm() { const [model] = useState(() => Model baru(surveySchema));
const mutasi = useMutation({ mutasiFn: async (data) => { const res = menunggu pengambilan("/api/pesanan", { metode: "POSTING", header: { "Tipe Konten": "application/json" }, isi: JSON.stringify(data), }); if (!res.ok) throw new Error("Gagal mengirimkan"); kembalikan res.json(); }, });
const mutasiRef = useRef(mutasi); mutasiRef.saat ini = mutasi; useEffect(() => { const handler = (pengirim) => mutasiRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref menghindari registrasi ulang handler setiap render (perubahan identitas objek mutasi)
kembali (
<>
Lihat Pen SurveyJS-03-SurveyJS [bercabang] pada kepunahan keenam.
onComplete diaktifkan ketika pengguna mencapai akhir halaman terakhir yang terlihat. Jadi jika total tidak pernah melewati 100 dan halaman ulasan dilewati, halaman tersebut masih aktif dengan benar karena SurveyJS mengevaluasi visibilitas sebelum memutuskan apa arti “halaman terakhir”. Kemudian, sender.data berisi semua jawaban beserta nilai terhitung (subtotal, pajak, total) sebagai bidang kelas satu, sehingga payload API identik dengan versi RHF yang dirakit secara manual di onSubmit. ItuPola mutasiRef adalah pola yang sama yang dapat Anda gunakan di mana pun Anda memerlukan pengendali peristiwa yang stabil atas nilai yang berubah pada setiap render - tidak ada yang khusus untuk SurveyJS.
Komponen React tidak lagi mengandung logika bisnis sama sekali. Tidak ada useWatch, tidak ada JSX bersyarat, tidak ada penghitung langkah, tidak ada rantai useMemo, tidak ada superRefine. React melakukan kelebihannya: merender komponen dan menghubungkannya ke panggilan API. Apa yang Keluar dari React?
Kekhawatiran Tumpukan RHF SurveiJS Visibilitas cabang BEJ terlihatJika Nilai turunan gunakanWatch / gunakanMemo ekspresi Aturan lintas bidang sangat halus Kondisi skema Navigasi keadaan langkah Halaman terlihatJika Lokasi aturan Didistribusikan ke seluruh file Terpusat dalam skema
Apa yang tersisa di React adalah tata letak, gaya, pengkabelan pengiriman, dan integrasi aplikasi, yang berarti, hal-hal yang sebenarnya dirancang untuk React. Segala sesuatu yang lain dipindahkan ke dalam skema, dan karena skema hanyalah objek JSON, skema tersebut dapat disimpan dalam database, dibuat versinya secara independen dari kode aplikasi Anda, atau diedit melalui peralatan internal tanpa memerlukan penerapan. Manajer produk yang perlu mengubah ambang batas yang memicu halaman peninjauan dapat melakukannya tanpa menyentuh komponen tersebut. Hal ini merupakan perbedaan operasional yang berarti bagi tim yang perilaku formulirnya sering berubah dan tidak selalu didorong oleh teknisi. Kapan Menggunakan Setiap Pendekatan? Inilah aturan praktis yang cocok untuk saya: bayangkan menghapus formulir seluruhnya. Apa yang akan hilang darimu?
Jika itu layar, Anda menginginkan formulir berbasis komponen. Jika logika bisnis, seperti ambang batas, aturan percabangan, dan persyaratan kondisional yang mengkodekan keputusan nyata, Anda memerlukan mesin skema.
Demikian pula, jika perubahan yang Anda terima sebagian besar berkaitan dengan label, bidang, dan tata letak, RHF akan membantu Anda dengan baik. Jika menyangkut kondisi, hasil, dan aturan yang mungkin perlu disesuaikan oleh operasi atau tim hukum Anda pada Selasa sore tanpa mengajukan tiket, model skema dengan SurveyJS adalah pilihan yang lebih tepat. Kedua pendekatan ini sebenarnya tidak saling bersaing. Aturan-aturan tersebut menangani berbagai jenis masalah, dan kesalahan yang perlu dihindari adalah ketidakcocokan abstraksi dengan bobot logika – memperlakukan sistem aturan seperti sebuah komponen karena itulah alat yang umum digunakan, atau menggunakan mesin kebijakan karena sebuah formulir berkembang menjadi tiga langkah dan memperoleh bidang bersyarat. Bentuk yang kami buat di sini sengaja ditempatkan di dekat batas, cukup rumit untuk memperlihatkan perbedaannya tetapi tidak terlalu ekstrem sehingga perbandingannya terasa dicurangi. Sebagian besar bentuk nyata yang menjadi sulit digunakan dalam basis kode Anda mungkin berada di dekat batas yang sama, dan pertanyaannya biasanya adalah apakah ada yang menyebutkan nama sebenarnya dari bentuk tersebut. Gunakan React Hook Form + Zod ketika:
Formulir berorientasi CRUD; Logikanya dangkal dan didorong oleh UI; Insinyur memiliki semua perilaku; Backend tetap menjadi sumber kebenaran.
Gunakan SurveyJS ketika:
Formulir menyandikan keputusan bisnis; Peraturan berkembang secara independen dari UI; Logika harus terlihat, dapat diaudit, atau dibuat versinya; Non-insinyur mempengaruhi perilaku; Formulir yang sama harus dijalankan di beberapa frontend.