Acest articol este sponsorizat de SurveyJS Există un model mental pe care majoritatea dezvoltatorilor React îl împărtășesc fără să-l discute vreodată cu voce tare. Aceste forme ar trebui să fie întotdeauna componente. Aceasta înseamnă o stivă ca:
Formular React Hook pentru statul local (redari minime, înregistrare ergonomică a câmpului, interacțiune imperativă). Zod pentru validare (corectitudinea intrării, validarea limitelor, parsare sigură de tip). React Query pentru backend: trimitere, reîncercări, stocare în cache, sincronizare server și așa mai departe.
Și pentru marea majoritate a formularelor - ecranele dvs. de conectare, paginile de setări, modalele CRUD - acest lucru funcționează foarte bine. Fiecare piesă își face treaba, compun curat și puteți trece la părțile aplicației dvs. care vă diferențiază de fapt produsul. Dar din când în când, un formular începe să acumuleze lucruri cum ar fi reguli de vizibilitate care depind de răspunsurile anterioare sau valori derivate care trec în cascadă prin trei câmpuri. Poate chiar pagini întregi care ar trebui sărite sau afișate pe baza unui total cumulat. Primul condiționat îl gestionați cu un useWatch și o ramură inline, ceea ce este bine. Apoi altul. Apoi apelați la superRefine pentru a codifica regulile încrucișate pe care schema dvs. Zod nu le poate exprima în mod normal. Apoi, navigarea pasilor începe să scurgă logica de afaceri. La un moment dat, te uiți la ceea ce ai construit și realizezi că formularul nu mai este cu adevărat UI. Este mai mult un proces de decizie, iar arborele componente este exact locul în care s-a întâmplat să îl stocați. Aici cred că modelul mental al formelor din React se defectează și nu este vina nimănui. Stiva RHF + Zod este excelentă pentru ceea ce a fost conceput. Problema este că avem tendința de a continua să-l folosim peste punctul în care abstracțiile sale se potrivesc cu problema, deoarece alternativa necesită un mod complet diferit de a gândi formele. Acest articol este despre această alternativă. Pentru a arăta acest lucru, vom construi exact același formular în mai mulți pași de două ori:
Cu React Hook Form + Zod conectat la React Query pentru trimitere, Cu SurveyJS, care tratează un formular ca date - o simplă schemă JSON - mai degrabă decât un arbore de componente.
Aceleași cerințe, aceeași logică condiționată, același apel API la sfârșit. Apoi vom mapa exact ce s-a mutat și ce a rămas și vom stabili o modalitate practică de a decide ce model ar trebui să utilizați și când. Formularul pe care îl construim:
Acest formular va folosi un flux în 4 pași: Pasul 1: Detalii
Prenume (obligatoriu), E-mail (obligatoriu, format valid).
Pasul 2: Comanda
pret unitar, cantitate, Cota de impozitare, Derivat: Subtotal, impozit, Total.
Pasul 3: Cont și feedback
Ai un cont? (Da/Nu) Dacă da → nume de utilizator + parolă, ambele sunt necesare. Dacă Nu → e-mail deja colectat la pasul 1.
Evaluare de satisfacție (1–5) Dacă ≥ 4 → întreabă „Ce ți-a plăcut?” Dacă ≤ 2 → întrebați „Ce putem îmbunătăți?”
Pasul 4: revizuire
Apare numai dacă total >= 100 Depunerea finală.
Acest lucru nu este extrem. Dar este suficient pentru a expune diferențele arhitecturale. Partea 1: Acționat de componente (Formă React Hook + Zod) Instalare npm instalează react-hook-form zod @hookform/resolvers @tanstack/react-query
Schema Zod Să începem cu schema Zod, pentru că acolo se stabilește de obicei forma formei. Pentru primii doi pași — detalii personale și comenzi de intrare — totul este simplu: șiruri necesare, numere cu minime și o enumerare. Partea interesantă începe când încerci să exprimi regulile condiționale.
import { z } din „zod”;
export const formSchema = z.object({ firstName: z.string().min(1, "Obligatori"), email: z.string().email("E-mail nevalid"), pret: z.number().min(0), cantitate: z.number().min(1), taxRate: z.number(), hasAccount: "["]), username:", z.string().optional(), parola: z.string().optional(), satisfacție: z.number().min(1).max(5), positiveFeedback: z.string().optional(), improvementFeedback: z.string().optional(),}).superRefine((data, ctx) => { if (data, ctx) => { if (data, ctx) => {) === "YAccount. ctx.addIssue({ cod: "personalizat", cale: ["nume utilizator"], mesaj: "Necesar" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ cod: "personalizat", cale: ["parolă"], mesaj: "Min 6 caractere}"});
if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ cod: "custom", cale: ["positiveFeedback"], mesaj: "Vă rugăm să distribuiți ceea ce v-a plăcut" }); }
dacă (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ cod: „personalizat”, cale:[„improvementFeedback”], mesaj: „Vă rugăm să ne spuneți ce să îmbunătățim” }); }});
tip de export FormData = z.infer
Observați că numele de utilizator și parola sunt introduse ca opțional() chiar dacă sunt necesare condiționat, deoarece schema la nivel de tip a lui Zod descrie forma obiectului, nu regulile care guvernează când contează câmpurile. Cerința condiționată trebuie să trăiască în interiorul superRefine, care rulează după validarea formei și are acces la obiectul complet. Acea separare nu este un defect; tocmai pentru asta este proiectat instrumentul: superRefine este locul în care merge logica cross-field atunci când nu poate fi exprimată în structura schemei în sine. Ceea ce este de asemenea notabil aici este ceea ce această schemă nu exprimă. Nu are nici un concept de pagini, nici un concept despre câmpurile care sunt vizibile în ce moment și nici un concept de navigare. Toate acestea vor trăi în altă parte. Componenta formularului
import { useForm, useWatch } din „react-hook-form”;import { zodResolver } din „@hookform/resolvers/zod”;import { useMutation } din „@tanstack/react-query”;import { useState, useMemo } din „react”;import { formSchema, tip „Form./schema”;
const PASI = ["detalii", "comanda", "cont", "revizuire"];
tip OrderPayload = FormData & { subtotal: număr; taxă: număr; total: număr };
funcția de export RHFMultiStepForm() { const [pas, setStep] = useState(0);
const mutation = useMutation({ mutationFn: asincron (sarcină utilă: OrderPayload) => { const res = așteaptă preluarea ("/api/comenzi", { metoda: "POST", anteturi: { "Content-Type": "application/json" }, body: JSON.stringify(sarcină utilă), }); if (!res.ok) throw new Error("Eșuat la trimitere"); return res.json(); }, });
const { register, control, handleSubmit, formState: { errors }, } = useForm
return (
);}Consultați Pen SurveyJS-03-RHF [furcat] de sixthextinction. Se întâmplă destul de multe aici și merită să încetinești pentru a observa unde au ajuns lucrurile.
Valorile derivate - subtotal, impozit, total - sunt calculate în componentă prin useWatch și useMemo, deoarece depind de valorile câmpului live și nu există un alt loc natural pentru ele. Regulile de vizibilitate pentru numele de utilizator, parola, feedback-ul pozitiv și feedback-ul îmbunătățit trăiesc în JSX ca condiționale inline. Logica de ignorare a pașilor - pagina de revizuire care apare numai când totalul >= 100 - este încorporată în variabila showSubmit și condiția de randare la pasul 3. Navigarea în sine este doar un contor useState pe care îl incrementăm manual. React Query gestionează reîncercări, memorarea în cache și invalidare. Formularul doar apelează mutation.mutate cu date validate.
Nimic din toate acestea nu este greșit, în sine. Acesta este încă React idiomatic, iar componenta este destul de performantă datorită modului în care RHF izolează redările. Dar dacă ar fi să predați asta cuiva care nu a scris-o și să-i cereți să explice în ce condiții apare pagina de revizuire, ar trebui să urmărească prin showSubmit, condiția de randare a pasului 3 și logica butonului de navigare - trei locuri separate - pentru a reconstrui o regulă care ar fi putut fi menționată într-un singur rând. Forma funcționează, da, dar comportamentul nu este cu adevărat inspectabil ca sistem. Trebuie executat mental. Mai important, schimbarea acestuia necesită implicarea inginerească. Chiar și o mică modificare, cum ar fi ajustarea când apare pasul de revizuire, înseamnă editarea componentei, actualizarea validării, deschiderea unei cereri de extragere, așteptarea revizuirii și implementarea din nou. Partea 2: bazată pe schemă (SurveyJS) Acum să construim același flux folosind o schemă. Instalare npm install survey-core survey-react-ui @tanstack/react-query
survey-coreMotorul de rulare independent de platformă, licențiat de MIT, care alimentează redarea formularelor SurveyJS - partea la care ne pasă aici. Este nevoie de o schemă JSON, construiește un model intern din aceasta și se ocupă de tot ceea ce altfel ar trăi în componenta dvs. React: evaluarea expresiilor de vizibilitate, calcularea valorilor derivate, gestionarea stării paginii, urmărirea validării și deciderea ce înseamnă „complet”, având în vedere ce pagini au fost afișate efectiv.
survey-react-uiLayerul UI / de randare care conectează modelul respectiv la React. Este, în esență, o componentă
Împreună, vă oferă un timp de rulare complet funcțional, cu mai multe pagini, fără a scrie o singură linie de flux de control. Formatul schemei în sine este, așa cum s-a spus mai înainte, doar un JSON - fără DSL sau ceva proprietar. Îl puteți integra, importa dintr-un fișier, îl puteți prelua dintr-un API sau îl puteți stoca într-o coloană a bazei de date și îl puteți hidrata în timpul execuției. Aceeași formă, ca și date Iată aceeași formă, de data aceasta exprimată ca obiect JSON. Schema definește totul: structură, validare, reguli de vizibilitate, calcule derivate, navigare pe pagină - și o predă unui Model care o evaluează în timpul execuției. Iată cum arată în întregime:
export const surveySchema = { title: „Fluxul de comandă”, showProgressBar: „top”, pagini: [ { nume: „detalii”, elemente: [ { type: „text”, nume: „firstName”, isRequired: true }, { type: „text”, name: „email”, inputType: „e-mail”, isRequired: true, validators: [{} „type: „în e-mail”] nume: „comanda”, elemente: [ { tip: „text”, nume: „preț”, inputType: „număr”, defaultValue: 0 }, { tip: „text”, nume: „cantitate”, inputType: „număr”, defaultValue: 1}, { tip: „dropdown”,nume: „taxRate”, defaultValue: 0,1, opțiuni: [ { valoare: 0,05, text: „5%” }, { valoare: 0,1, text: „10%” }, { valoare: 0,15, text: „15%” } ] }, { tip: „expresie”, nume: „subtotal”, expresie: „cantitate:} „preț”, expresie: {cantitate:} „preț” expresie: "tax", expresie: "{subtotal} {taxRate}" }, { tip: "expresie", nume: "total", expresie: "{subtotal} + {tax}" } ] }, { nume: "cont", elemente: [ { type: "radiogroup", nume: "hasAccount", opțiuni: ["Da", "Nu""] }, {:":"nume"] }, {:":"nume text" „{hasAccount} = „Da””, isRequired: true }, { type: „text”, nume: „parolă”, inputType: „parolă”, visibleIf: „{hasAccount} = „Yes””, isRequired: true, validatori: [{ type: „text”, minLength: 6, text: „Min 6 characters},” {}, „factions” rateMin: 1, rateMax: 5 }, { type: "comentare", nume: "positiveFeedback", visibleIf: "{satisfacție} >= 4" }, { tip: "comentare", nume: "improvementFeedback", visibleIf: "{satisfacție} <= 2" } ] }, {name: >: "re vizualizare", {}, elemente vizibile: ": 0, total: 0 : 0 [] } ]};
Comparați acest lucru cu versiunea RHF pentru un moment.
Blocul superRefine care a cerut condiționat numele de utilizator și parola a dispărut. visibleIf: „{hasAccount} = „Da” combinat cu isRequired: true se ocupă de ambele preocupări împreună, în câmpul însuși, unde te-ai aștepta să le găsești. Lanțul useWatch + useMemo care a calculat subtotalul, impozitul și totalul este înlocuit cu trei câmpuri de expresie care se referă unul la celălalt după nume. Condiția paginii de revizuire, care în versiunea RHF a fost reconstruită doar prin urmărirea prin showSubmit, ramura de randare a pasului 3. Și, în sfârșit, logica butonului de navigare este o singură proprietate visibleIf pe obiectul pagină.
Aceeași logică există. Doar că schema îi oferă un loc de locuit unde este vizibil izolat, mai degrabă decât răspândit în întreaga componentă. De asemenea, rețineți că schema utilizează tipul: „expresie” pentru subtotal, taxă și total. Expresia este doar pentru citire și este utilizată în principal pentru a afișa valorile calculate. SurveyJS acceptă și tipul: „html” pentru conținut static, dar pentru valorile calculate, expresia este alegerea potrivită. Acum pentru partea React. Redare și transmitere Foarte simplu. Conectați onComplete la API-ul dvs. în același mod - prin useMutation sau simple fetch:
import { useState, useEffect, useRef } din „react”;import { useMutation } din „@tanstack/react-query”;import { Model } din „survey-core”;import { Survey } din „survey-react-ui”;import „survey-core/survey-core.css”;
funcția de export SurveyForm() { const [model] = useState(() => model nou (surveySchema));
const mutation = useMutation({ mutationFn: asincron (date) => { const res = așteaptă preluarea ("/api/comenzi", { metoda: "POST", anteturi: { "Content-Type": "application/json" }, body: JSON.stringify(date), }); if (!res.ok) throw new Error("Eșuat la trimitere"); return res.json(); }, });
const mutationRef = useRef(mutație); mutationRef.current = mutatie; useEffect(() => { const handler = (sender) => mutationRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref evită reînregistrarea handler-ului la fiecare randare (schimbări de identitate a obiectului de mutație)
intoarce (
<>
A se vedea Pen SurveyJS-03-SurveyJS [forked] de sixthextinction.
onComplete se declanșează când utilizatorul ajunge la sfârșitul ultimei pagini vizibile. Deci, dacă totalul nu depășește niciodată 100 și pagina de revizuire este omisă, se declanșează în continuare corect, deoarece SurveyJS evaluează vizibilitatea înainte de a decide ce înseamnă „ultima pagină”. Apoi, sender.data conține toate răspunsurile împreună cu valorile calculate (subtotal, taxă, total) ca câmpuri de primă clasă, astfel încât sarcina utilă API este identică cu cea a asamblată manual versiunea RHF în onSubmit. TheModelul mutationRef este același pentru care ați ajunge oriunde aveți nevoie de un handler de evenimente stabil peste o valoare care se modifică la fiecare randare - nimic specific pentru SurveyJS.
Componenta React nu mai conține deloc nicio logică de afaceri. Nu există useWatch, nici JSX condiționat, nici contor de pași, nici un lanț useMemo, nici superRafinare. React face ceea ce este de fapt bun: redarea unei componente și conectarea acesteia la un apel API. Ce sa mutat din reacție?
Îngrijorare Stiva RHF SurveyJS Vizibilitate filialele JSX vizibilDacă Valori derivate foloseșteWatch/useMemo expresie Reguli încrucișate superRafinare Condiții de schemă Navigare stare de pas Pagina vizibilăDacă Reguli de locație Distribuit în fișiere Centralizat în schemă
Ceea ce rămâne în React este aspectul, stilul, cablarea trimiterii și integrarea aplicației, adică lucrurile pentru care este proiectat de fapt React. Orice altceva a fost mutat în schemă și, deoarece schema este doar un obiect JSON, poate fi stocată într-o bază de date, versiunea independent de codul aplicației sau editată prin instrumente interne fără a necesita o implementare. Un manager de produs care trebuie să schimbe pragul care declanșează pagina de recenzie poate face asta fără să atingă componenta. Aceasta este o diferență operațională semnificativă pentru echipele în care comportamentul formei evoluează frecvent și nu este întotdeauna condus de ingineri. Când să folosiți fiecare abordare? Iată o regulă de bază care funcționează pentru mine: imaginați-vă că ștergeți formularul în întregime. Ce ai pierde?
Dacă este vorba de ecrane, doriți formulare bazate pe componente. Dacă este vorba de logica de afaceri, cum ar fi praguri, reguli de ramificare și cerințe condiționate care codifică decizii reale, vrei un motor de schemă.
În mod similar, dacă modificările care vin în calea dvs. se referă în principal la etichete, câmpuri și aspect, RHF vă va fi de folos. Dacă este vorba despre condiții, rezultate și reguli pe care echipa operațională sau juridică ar putea avea nevoie să le ajusteze într-o după-amiază de marți fără a depune un bilet, modelul de schemă cu SurveyJS este cel mai potrivit. Aceste două abordări nu sunt cu adevărat în competiție una cu cealaltă. Aceștia abordează diferite clase de probleme, iar greșeala care merită evitată este nepotrivirea abstracției cu greutatea logicii - tratarea unui sistem de reguli ca pe o componentă pentru că acesta este instrumentul familiar sau apelarea la un motor de politici deoarece o formă a crescut la trei pași și a dobândit un câmp condiționat. Forma pe care am construit-o aici se așează în mod deliberat lângă graniță, suficient de complexă pentru a expune diferența, dar nu atât de extremă încât comparația să pară trucată. Majoritatea formelor reale care au devenit greoaie în baza ta de cod se află probabil aproape de aceeași graniță, iar întrebarea este de obicei dacă cineva a numit ceea ce sunt de fapt. Folosiți React Hook Form + Zod când:
Formele sunt orientate spre CRUD; Logica este superficială și bazată pe UI; Inginerii dețin orice comportament; Backend-ul rămâne sursa adevărului.
Utilizați SurveyJS când:
Formularele codifică deciziile de afaceri; Regulile evoluează independent de UI; Logica trebuie să fie vizibilă, auditabilă sau versiunea; Non-inginerii influențează comportamentul; Același formular trebuie să ruleze pe mai multe interfețe.