Dit artikel wordt gesponsord door SurveyJS Er is een mentaal model dat de meeste React-ontwikkelaars delen zonder het ooit hardop te bespreken. Dat formulieren altijd componenten moeten zijn. Dit betekent een stapel als:
React Hook-formulier voor lokale staat (minimale herweergave, ergonomische veldregistratie, dwingende interactie). Zod voor validatie (invoercorrectheid, grensvalidatie, typeveilige parsing). Reageren Query voor backend: indiening, nieuwe pogingen, caching, serversynchronisatie, enzovoort.
En voor de overgrote meerderheid van de formulieren – uw inlogschermen, uw instellingenpagina’s, uw CRUD-modaliteiten – werkt dit heel goed. Elk onderdeel doet zijn werk, ze zijn netjes samengesteld en u kunt doorgaan met de delen van uw toepassing die uw product daadwerkelijk onderscheiden. Maar af en toe begint een formulier zaken te verzamelen, zoals zichtbaarheidsregels die afhankelijk zijn van eerdere antwoorden, of afgeleide waarden die door drie velden lopen. Misschien zelfs hele pagina's die moeten worden overgeslagen of weergegeven op basis van een lopend totaal. Je handelt de eerste voorwaardelijke af met een useWatch en een inline branch, wat prima is. Dan nog een. Vervolgens grijpt u naar superRefine om veldoverschrijdende regels te coderen die uw Zod-schema niet op de normale manier kan uitdrukken. Vervolgens begint stapnavigatie bedrijfslogica te lekken. Op een gegeven moment kijk je naar wat je hebt gebouwd en besef je dat het formulier niet echt meer een gebruikersinterface is. Het is meer een besluitvormingsproces, en de componentenboom is precies waar je hem toevallig hebt opgeslagen. Dit is waar ik denk dat het mentale model voor formulieren in React kapot gaat, en dat is echt niemands schuld. De RHF + Zod-stack is uitstekend waarvoor hij is ontworpen. Het probleem is dat we de neiging hebben om het te blijven gebruiken voorbij het punt waarop de abstracties overeenkomen met het probleem, omdat het alternatief een geheel andere manier van denken over vormen vereist. Dit artikel gaat over dat alternatief. Om dit te laten zien, bouwen we tweemaal exact hetzelfde meerstapsformulier:
Met React Hook Form + Zod aangesloten op React Query voor indiening, Met SurveyJS, dat een formulier als gegevens behandelt (een eenvoudig JSON-schema) in plaats van als een componentenboom.
Dezelfde vereisten, dezelfde voorwaardelijke logica, dezelfde API-aanroep aan het einde. Vervolgens brengen we precies in kaart wat er is verplaatst en wat is gebleven, en leggen we een praktische manier uit om te beslissen welk model u moet gebruiken, en wanneer. Het formulier dat we bouwen:
Dit formulier gebruikt een stroom van 4 stappen: Stap 1: Details
Voornaam (verplicht), E-mail (vereist, geldig formaat).
Stap 2: Bestellen
Eenheidsprijs, Hoeveelheid, Belastingtarief, Afgeleid: Subtotaal, Belasting, Totaal.
Stap 3: Account & Feedback
Heeft u een account? (Ja/Nee) Indien Ja → gebruikersnaam + wachtwoord, beide vereist. Indien Nee → e-mail al verzameld in stap 1.
Tevredenheidsscore (1–5) Als ≥ 4 → vraag “Wat vond je leuk?” Als ≤ 2 → vraag “Wat kunnen we verbeteren?”
Stap 4: Beoordeling
Verschijnt alleen als totaal >= 100 Definitieve indiening.
Dit is niet extreem. Maar het is genoeg om architecturale verschillen bloot te leggen. Deel 1: Component-aangedreven (React Hook Form + Zod) Installatie npm installeer react-hook-form zod @hookform/resolvers @tanstack/react-query
Zod-schema Laten we beginnen met het Zod-schema, omdat daar meestal de vorm van de vorm wordt vastgesteld. Voor de eerste twee stappen – persoonlijke gegevens en orderinvoer – is alles eenvoudig: vereiste reeksen, getallen met minima en een opsomming. Het interessante deel begint wanneer je de voorwaardelijke regels probeert uit te drukken.
importeer { z } uit "zod";
export const formSchema = z.object({ firstName: z.string().min(1, "Vereist"), email: z.string().email("Ongeldige e-mail"), prijs: z.number().min(0), hoeveelheid: z.number().min(1), taxRate: z.number(), hasAccount: z.enum(["Ja", "Nee"]), gebruikersnaam: z.string().optioneel(), wachtwoord: z.string().optioneel(), tevredenheid: z.number().min(1).max(5), positiveFeedback: z.string().optioneel(), verbeteringFeedback: z.string().optioneel(),}).superRefine((data, ctx) => { if (data.hasAccount === "Ja") { if (!data.username) { ctx.addIssue({ code: "custom", pad: ["gebruikersnaam"], bericht: "Vereist" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "aangepast", pad: ["wachtwoord"], bericht: "Min. 6 tekens" } });
if (data.satisfaction >= 4 && !data.positiveFeedback) {ctx.addIssue({ code: "custom", pad: ["positiveFeedback"], bericht: "Deel alstublieft wat u leuk vond" }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "aangepast", pad:["improvementFeedback"], bericht: "Vertel ons alstublieft wat we kunnen verbeteren" }); }});
exporttype FormData = z.infer
Merk op dat gebruikersnaam en wachtwoord als optioneel() worden getypt, ook al zijn ze voorwaardelijk vereist, omdat het typeniveauschema van Zod de vorm van het object beschrijft, en niet de regels die bepalen wanneer velden er toe doen. De voorwaardelijke vereiste moet in superRefine leven, die wordt uitgevoerd nadat de vorm is gevalideerd en toegang heeft tot het volledige object. Die scheiding is geen fout; het is precies waarvoor de tool is ontworpen: superRefine is waar veldoverschrijdende logica naartoe gaat als deze niet in de schemastructuur zelf kan worden uitgedrukt. Wat hier ook opvalt, is wat dit schema niet uitdrukt. Het heeft geen concept van pagina's, geen concept van welke velden op welk punt zichtbaar zijn, en geen concept van navigatie. Dat zal allemaal ergens anders leven. Vormcomponent
importeer { useForm, useWatch } uit "react-hook-form"; importeer { zodResolver } uit "@hookform/resolvers/zod"; importeer { useMutation } uit "@tanstack/react-query"; importeer { useState, useMemo } uit "react"; importeer { formSchema, type FormData } uit "./schema";
const STEPS = ["details", "bestelling", "account", "beoordeling"];
type OrderPayload = FormData & {subtotaal: nummer; belasting: aantal; totaal: aantal };
exportfunctie RHFMultiStepForm() {const [stap, setStep] = useState(0);
const mutatie = useMutation({ mutatieFn: async (payload: OrderPayload) => { const res = wachten op ophalen("/api/orders", { methode: "POST", headers: { "Contenttype": "applicatie/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error("Kan niet indienen"); retourneer res.json(); }, });
const { register, control, handleSubmit, formState: { fouten }, } = useForm
return (
);}Zie de Pen SurveyJS-03-RHF [gevorkt] door zesde uitsterven. Er gebeurt hier heel veel, en het is de moeite waard om te vertragen om op te merken waar de dingen terecht zijn gekomen.
De afgeleide waarden (subtotaal, belasting, totaal) worden in de component berekend via useWatch en useMemo, omdat ze afhankelijk zijn van live veldwaarden en er geen andere natuurlijke plaats voor hen is. De zichtbaarheidsregels voor gebruikersnaam, wachtwoord, positiveFeedback en ImprovementFeedback leven in JSX als inline conditionals. De logica voor het overslaan van stappen (de beoordelingspagina verschijnt alleen als het totaal >= 100) is ingebed in de showSubmit-variabele en de weergavevoorwaarde in stap 3. Navigatie zelf is slechts een useState-teller die we handmatig verhogen. React Query verwerkt nieuwe pogingen, caching en invalidatie. Het formulier roept alleen mutatie.mutate aan met gevalideerde gegevens.
Niets van dit alles is per se verkeerd. Dit is nog steeds een idiomatische React, en de component presteert behoorlijk goed dankzij de manier waarop RHF re-renders isoleert. Maar als je dit aan iemand zou overhandigen die het niet heeft geschreven en hem zou vragen uit te leggen onder welke omstandigheden de recensiepagina verschijnt, zouden ze via showSubmit, de rendervoorwaarde van stap 3 en de navigatieknoplogica (drie afzonderlijke plaatsen) moeten nagaan om een regel te reconstrueren die op één regel had kunnen worden vermeld. De vorm werkt, ja, maar het gedrag is als systeem niet echt controleerbaar. Het moet mentaal worden uitgevoerd. Belangrijker nog is dat het veranderen ervan technische betrokkenheid vereist. Zelfs een kleine aanpassing, zoals het aanpassen wanneer de beoordelingsstap verschijnt, betekent het bewerken van de component, het bijwerken van de validatie, het openen van een pull-verzoek, het wachten op beoordeling en het opnieuw implementeren ervan. Deel 2: Schemagestuurd (SurveyJS) Laten we nu dezelfde stroom bouwen met behulp van een schema. Installatie npm installeer survey-core survey-react-ui @tanstack/react-query
survey-coreDe door MIT gelicentieerde platformonafhankelijke runtime-engine die de formulierweergave van SurveyJS aanstuurt – het deel waar we hier om geven. Er is een JSON-schema voor nodig, er wordt een intern model van gemaakt en het regelt alles wat anders in uw React-component zou voorkomen: het evalueren van zichtbaarheidsuitdrukkingen, het berekenen van afgeleide waarden, het beheren van de paginastatus, het bijhouden van validatie en het beslissen wat ‘voltooid’ betekent, gegeven welke pagina’s daadwerkelijk zijn weergegeven.
survey-react-uiDe UI/renderinglaag die dat model met React verbindt. Het is in wezen een
Samen bieden ze u een volledig functionele formulierruntime van meerdere pagina's zonder dat u ook maar één regel voor de besturingsstroom hoeft te schrijven. Het schemaformaat zelf is, zoals eerder gezegd, slechts een JSON – geen DSL of iets dergelijks. U kunt het inline plaatsen, importeren uit een bestand, ophalen uit een API, of opslaan in een databasekolom en tijdens runtime hydrateren. Hetzelfde formulier, als gegevens Hier is hetzelfde formulier, dit keer uitgedrukt als een JSON-object. Het schema definieert alles: structuur, validatie, zichtbaarheidsregels, afgeleide berekeningen, paginanavigatie - en geeft het door aan een model dat het tijdens runtime evalueert. Hier ziet u hoe dat er volledig uitziet:
export const surveySchema = { title: "Orderstroom", showProgressBar: "top", pagina's: [ { naam: "details", elementen: [ { type: "text", naam: "firstName", isRequired: true }, { type: "text", naam: "email", inputType: "email", isRequired: true, validators: [{ type: "email", tekst: "Ongeldige e-mail" }] } ] }, { naam: "order", elements: [ { type: "text", name: "price", inputType: "number", defaultValue: 0 }, { type: "text", name: "quantity", inputType: "number", defaultValue: 1 }, { type: "dropdown",naam: "taxRate", defaultValue: 0.1, keuzes: [ { waarde: 0.05, tekst: "5%" }, { waarde: 0.1, tekst: "10%" }, { waarde: 0.15, tekst: "15%" } ] }, { type: "expressie", naam: "subtotaal", expressie: "{prijs} {hoeveelheid}" }, { type: "expressie", naam: "tax", expressie: "{subtotal} {taxRate}" }, { type: "expression", naam: "total", expressie: "{subtotal} + {tax}" } ] }, { naam: "account", elementen: [ { type: "radiogroup", naam: "hasAccount", keuzes: ["Ja", "Nee"] }, { type: "text", naam: "gebruikersnaam", zichtbaarIf: "{hasAccount} = 'Ja'", isRequired: true }, { type: "text", naam: "wachtwoord", inputType: "wachtwoord", zichtbaarIf: "{hasAccount} = 'Ja'", isRequired: true, validators: [{ type: "text", minLength: 6, tekst: "Min 6 tekens" }] }, { type: "rating", naam: "satisfaction", rateMin: 1, rateMax: 5 }, { type: "commentaar", naam: "positiveFeedback", zichtbaarIf: "{satisfaction} >= 4" }, { type: "commentaar", naam: "improvementFeedback", zichtbaarIf: "{satisfaction} <= 2" } ] }, { naam: "review", zichtbaarIf: "{totaal} >= 100", elementen: [] } ]};
Vergelijk dit even met de RHF-versie.
Het superRefine-blok dat voorwaardelijk vereiste gebruikersnaam en wachtwoord vereiste, is verdwenen. zichtbaarIf: "{hasAccount} = 'Ja'" gecombineerd met isRequired: true behandelt beide problemen samen, op het veld zelf, waar u ze zou verwachten te vinden. De useWatch + useMemo-keten die het subtotaal, de belasting en het totaal berekende, wordt vervangen door drie expressievelden die bij naam naar elkaar verwijzen. De toestand van de beoordelingspagina, die in de RHF-versie alleen reconstrueerbaar was door het traceren via showSubmit, de rendervertakking van stap 3. En ten slotte is de logica van de navigatieknop één enkele eigenschap zichtbaarIf voor het paginaobject.
Dezelfde logica is er. Het is alleen zo dat het schema het een plek geeft om te leven waar het afzonderlijk zichtbaar is, in plaats van verspreid over het onderdeel. Houd er ook rekening mee dat het schema type: 'expression' gebruikt voor subtotaal, belasting en totaal. Expressie is alleen-lezen en wordt voornamelijk gebruikt om berekende waarden weer te geven. SurveyJS ondersteunt ook type: 'html' voor statische inhoud, maar voor berekende waarden is expressie de juiste keuze. Nu voor de React-kant. Weergave en indiening Heel eenvoudig. Maak op dezelfde manier verbinding met uw API - via useMutation of gewoon ophalen:
importeer { useState, useEffect, useRef } uit "react"; importeer { useMutation } uit "@tanstack/react-query"; importeer { Model } uit "survey-core"; importeer { Survey } uit "survey-react-ui"; importeer "survey-core/survey-core.css";
exportfunctie SurveyForm() { const [model] = useState(() => nieuw model(enquêteschema));
const mutatie = useMutation({ mutatieFn: async (gegevens) => { const res = wachten op ophalen("/api/orders", { methode: "POST", headers: { "Contenttype": "applicatie/json" }, body: JSON.stringify(gegevens), }); if (!res.ok) throw new Error("Kan niet indienen"); retourneer res.json(); }, });
const mutatieRef = gebruikRef(mutatie); mutatieRef.current = mutatie; useEffect(() => { const handler = (afzender) => mutatieRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref vermijdt het opnieuw registreren van de handler bij elke weergave (wijzigingen in de identiteit van het mutatieobject)
terug (
<>
Zie de Pen SurveyJS-03-SurveyJS [gevorkt] door zesdeextinction.
onComplete wordt geactiveerd wanneer de gebruiker het einde van de laatst zichtbare pagina bereikt. Dus als het totaal nooit de 100 overschrijdt en de beoordelingspagina wordt overgeslagen, wordt deze nog steeds correct geactiveerd omdat SurveyJS de zichtbaarheid evalueert voordat wordt besloten wat “laatste pagina” betekent. Vervolgens bevat sender.data alle antwoorden samen met de berekende waarden (subtotaal, belasting, totaal) als eersteklas velden, zodat de API-payload identiek is aan wat de RHF-versie handmatig heeft samengesteld in onSubmit. DeHet MutationRef-patroon is hetzelfde als waar je een stabiele gebeurtenishandler nodig hebt voor een waarde die bij elke weergave verandert - er is niets SurveyJS-specifiek aan.
De React-component bevat helemaal geen bedrijfslogica meer. Er is geen useWatch, geen voorwaardelijke JSX, geen stappenteller, geen useMemo-keten, geen superRefine. React doet waar het eigenlijk goed in is: een component renderen en deze verbinden met een API-aanroep. Wat is er uit React gegaan?
Bezorgdheid RHF-stapel EnquêteJS Zichtbaarheid JSX-takken zichtbaarAls Afgeleide waarden gebruikBekijk / gebruikMemo expressie Regels voor meerdere velden superVerfijn Schemavoorwaarden Navigatie stap staat Pagina zichtbaarAls Regel locatie Verdeeld over bestanden Gecentraliseerd in het schema
Wat in React blijft, is de lay-out, styling, bedrading voor indiening en app-integratie, dat wil zeggen de dingen waarvoor React eigenlijk is ontworpen. Al het andere is naar het schema verplaatst, en omdat het schema slechts een JSON-object is, kan het worden opgeslagen in een database, onafhankelijk van uw applicatiecode worden bijgewerkt, of worden bewerkt via interne tools zonder dat een implementatie nodig is. Een productmanager die de drempel moet wijzigen die de beoordelingspagina activeert, kan dat doen zonder de component aan te raken. Dat is een betekenisvol operationeel verschil voor teams waar vormgedrag regelmatig evolueert en niet altijd wordt aangestuurd door ingenieurs. Wanneer elke aanpak gebruiken? Hier is een goede vuistregel die voor mij werkt: stel je voor dat je het formulier volledig verwijdert. Wat zou je verliezen?
Als het schermen zijn, wil je componentgestuurde formulieren. Als het bedrijfslogica is, zoals drempels, vertakkingsregels en voorwaardelijke vereisten die echte beslissingen coderen, heb je een schema-engine nodig.
Op dezelfde manier, als de veranderingen die op u afkomen voornamelijk betrekking hebben op labels, velden en lay-out, zal RHF u prima van dienst zijn. Als het gaat om voorwaarden, uitkomsten en regels die uw operationele of juridische team mogelijk op dinsdagmiddag moet aanpassen zonder een ticket in te dienen, is het schemamodel met SurveyJS de meest eerlijke oplossing. Deze twee benaderingen concurreren niet echt met elkaar. Ze richten zich op verschillende soorten problemen, en de fout die het vermijden waard is, is het verkeerd afstemmen van de abstractie op het gewicht van de logica: een regelsysteem als een component behandelen omdat dat het bekende instrument is, of naar een beleidsmotor grijpen omdat een formulier uit drie stappen groeide en een voorwaardelijk veld kreeg. De vorm die we hier hebben gebouwd, ligt bewust dichtbij de grens, complex genoeg om het verschil bloot te leggen, maar niet zo extreem dat de vergelijking vervalst lijkt. De meeste echte vormen die in uw codebase onpraktisch zijn geworden, bevinden zich waarschijnlijk in de buurt van diezelfde grens, en de vraag is meestal alleen of iemand heeft genoemd wat ze werkelijk zijn. Gebruik React Hook Form + Zod wanneer:
Formulieren zijn CRUD-georiënteerd; Logica is oppervlakkig en UI-gestuurd; Ingenieurs zijn eigenaar van al het gedrag; Backend blijft de bron van de waarheid.
Gebruik SurveyJS wanneer:
Formulieren coderen zakelijke beslissingen; Regels evolueren onafhankelijk van de gebruikersinterface; Logica moet zichtbaar, controleerbaar of voorzien van een versiebeheer zijn; Niet-ingenieurs beïnvloeden gedrag; Hetzelfde formulier moet over meerdere frontends lopen.