Dieser Artikel wird von SurveyJS gesponsert Es gibt ein mentales Modell, das die meisten React-Entwickler teilen, ohne es jemals laut zu diskutieren. Dass Formen immer Komponenten sein sollen. Dies bedeutet einen Stapel wie:
Reagieren Sie auf das Hook-Formular für den lokalen Status (minimale erneute Renderings, ergonomische Feldregistrierung, zwingende Interaktion). Zod zur Validierung (Eingabekorrektheit, Grenzvalidierung, typsicheres Parsen). Reagieren Sie auf die Abfrage für das Backend: Übermittlung, Wiederholungsversuche, Caching, Serversynchronisierung usw.
Und für die überwiegende Mehrheit der Formulare – Ihre Anmeldebildschirme, Ihre Einstellungsseiten, Ihre CRUD-Modalitäten – funktioniert das wirklich gut. Jedes Teil erfüllt seine Aufgabe, es setzt sich sauber zusammen und Sie können mit den Teilen Ihrer Anwendung fortfahren, die Ihr Produkt tatsächlich auszeichnen. Aber hin und wieder fängt ein Formular an, Dinge wie Sichtbarkeitsregeln anzusammeln, die von früheren Antworten abhängen, oder abgeleitete Werte, die durch drei Felder kaskadieren. Vielleicht sogar ganze Seiten, die übersprungen oder basierend auf einer laufenden Summe angezeigt werden sollten. Sie behandeln die erste Bedingung mit einer useWatch und einem Inline-Zweig, was in Ordnung ist. Dann noch einer. Dann greifen Sie zu superRefine, um feldübergreifende Regeln zu kodieren, die Ihr Zod-Schema nicht auf normale Weise ausdrücken kann. Dann beginnt die Schrittnavigation, Geschäftslogik preiszugeben. Irgendwann schauen Sie sich an, was Sie erstellt haben, und stellen fest, dass das Formular nicht mehr wirklich eine Benutzeroberfläche ist. Es handelt sich eher um einen Entscheidungsprozess, und der Komponentenbaum ist genau der Ort, an dem Sie ihn gespeichert haben. Hier bricht meiner Meinung nach das mentale Modell für Formen in React zusammen, und daran ist wirklich niemand schuld. Der RHF + Zod-Stack erfüllt hervorragend das, wofür er entwickelt wurde. Das Problem besteht darin, dass wir dazu neigen, es über den Punkt hinaus weiter zu verwenden, an dem seine Abstraktionen mit dem Problem übereinstimmen, weil die Alternative eine völlig andere Denkweise über Formen erfordert. In diesem Artikel geht es um diese Alternative. Um dies zu zeigen, erstellen wir zweimal genau dasselbe mehrstufige Formular:
Wenn React Hook Form + Zod zur Übermittlung mit React Query verbunden ist, Mit SurveyJS, das ein Formular als Daten – ein einfaches JSON-Schema – und nicht als Komponentenbaum behandelt.
Gleiche Anforderungen, gleiche bedingte Logik, gleicher API-Aufruf am Ende. Anschließend erfassen wir genau, was umgezogen ist und was geblieben ist, und erläutern eine praktische Methode für die Entscheidung, welches Modell Sie wann verwenden sollten. Das Formular, das wir erstellen:
Dieses Formular verwendet einen 4-Schritte-Ablauf: Schritt 1: Details
Vorname (erforderlich), E-Mail (erforderlich, gültiges Format).
Schritt 2: Bestellen
Stückpreis, Menge, Steuersatz, Abgeleitet: Zwischensumme, Steuer, Insgesamt.
Schritt 3: Konto und Feedback
Haben Sie ein Konto? (Ja/Nein) Wenn Ja → Benutzername + Passwort, beides erforderlich. Wenn Nein → E-Mail bereits in Schritt 1 erfasst.
Zufriedenheitsbewertung (1–5) Wenn ≥ 4 → fragen Sie „Was hat Ihnen gefallen?“ Wenn ≤ 2 → fragen Sie: „Was können wir verbessern?“
Schritt 4: Überprüfen
Erscheint nur, wenn die Summe >= 100 ist Endgültige Einreichung.
Das ist nicht extrem. Aber es reicht aus, um architektonische Unterschiede aufzudecken. Teil 1: Komponentengesteuert (React Hook Form + Zod) Installation npm install React-Hook-Form zod @hookform/resolvers @tanstack/react-query
Zod-Schema Beginnen wir mit dem Zod-Schema, denn dort wird normalerweise die Form des Formulars festgelegt. Für die ersten beiden Schritte – persönliche Daten und Bestelleingaben – ist alles unkompliziert: erforderliche Zeichenfolgen, Zahlen mit Mindestangaben und eine Aufzählung. Der interessante Teil beginnt, wenn Sie versuchen, die bedingten Regeln auszudrücken.
import { z } from „zod“;
export const formSchema = z.object({ firstName: z.string().min(1, "Required"), email: z.string().email("Invalid email"), price: z.number().min(0), amount: z.number().min(1), taxRate: z.number(), hasAccount: z.enum(["Yes", "No"]), username: z.string().optional(), Passwort: z.string().optional(), Zufriedenheit: z.number().min(1).max(5), positivesFeedback: z.string().optional(), VerbesserungFeedback: z.string().optional(),}).superRefine((data, ctx) => { if (data.hasAccount === "Yes") { if (!data.username) { ctx.addIssue({ code: "custom", path: ["username"], message: "Erforderlich" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ code: "custom", path: ["password"], message: "Min. 6 Zeichen" } }
if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ code: "custom", path: ["positiveFeedback"], message: "Bitte teilen Sie mit, was Ihnen gefallen hat" }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ code: "custom", path:["improvementFeedback"], Nachricht: "Bitte teilen Sie uns mit, was wir verbessern sollen" }); }});
export type FormData = z.infer
Beachten Sie, dass Benutzername und Passwort als optional() eingegeben werden, obwohl sie bedingt erforderlich sind, da Zods Schema auf Typebene die Form des Objekts beschreibt und nicht die Regeln, die regeln, wann Felder wichtig sind. Die bedingte Anforderung muss in superRefine enthalten sein, das nach der Validierung der Form ausgeführt wird und Zugriff auf das gesamte Objekt hat. Diese Trennung ist kein Fehler; Genau dafür ist das Tool konzipiert: SuperRefine ist der Ort, an dem feldübergreifende Logik eingesetzt wird, wenn sie nicht in der Schemastruktur selbst ausgedrückt werden kann. Bemerkenswert ist hier auch, was dieses Schema nicht ausdrückt. Es gibt kein Konzept für Seiten, kein Konzept dafür, welche Felder an welcher Stelle sichtbar sind und kein Konzept für die Navigation. All das wird woanders leben. Formularkomponente
import { useForm, useWatch } from „react-hook-form“;import { zodResolver } from „@hookform/resolvers/zod“;import { useMutation } from „@tanstack/react-query“;import { useState, useMemo } from „react“;import { formSchema, type FormData } from „./schema“;
const STEPS = ["details", "order", "account", "review"];
type OrderPayload = FormData & { subtotal: number; Steuer: Nummer; total: Zahl };
Exportfunktion RHFMultiStepForm() { const [step, setStep] = useState(0);
const mutation = useMutation({ mutationFn: async (Nutzlast: OrderPayload) => { const res = waiting fetch("/api/orders", { Methode: „POST“, Header: { "Content-Type": "application/json" }, Körper: JSON.stringify(Nutzlast), }); if (!res.ok) throw new Error("Fehler beim Senden"); return res.json(); }, });
const { register, control, handleSubmit, formState: { Fehler }, } = useForm
return (
);}Siehe die Pen SurveyJS-03-RHF [geforkt] von sixxtinction. Hier passiert ziemlich viel, und es lohnt sich, einen Gang zurückzuschalten, um zu bemerken, wo die Dinge gelandet sind.
Die abgeleiteten Werte – Zwischensumme, Steuer, Gesamt – werden in der Komponente über useWatch und useMemo berechnet, da sie von Live-Feldwerten abhängen und es keinen anderen natürlichen Ort für sie gibt. Die Sichtbarkeitsregeln für Benutzername, Passwort, positives Feedback und Verbesserungs-Feedback sind in JSX als Inline-Bedingungen verfügbar. Die Logik zum Überspringen von Schritten – die Überprüfungsseite wird nur angezeigt, wenn die Gesamtsumme >= 100 beträgt – ist in die Variable showSubmit und die Renderbedingung in Schritt 3 eingebettet. Die Navigation selbst ist lediglich ein useState-Zähler, den wir manuell erhöhen. React Query verarbeitet Wiederholungsversuche, Caching und Invalidierung. Das Formular ruft lediglich mutation.mutate mit validierten Daten auf.
Nichts davon ist per se falsch. Dies ist immer noch idiomatisches React, und die Komponente ist dank der Art und Weise, wie RHF Neu-Renderings isoliert, recht leistungsfähig. Wenn Sie dies jedoch jemandem geben würden, der es nicht geschrieben hat, und ihn bitten würden, zu erklären, unter welchen Bedingungen die Rezensionsseite angezeigt wird, müsste er showSubmit, die Renderbedingung für Schritt 3 und die Navigationsschaltflächenlogik – drei verschiedene Stellen – nachverfolgen, um eine Regel zu rekonstruieren, die in einer Zeile hätte angegeben werden können. Das Formular funktioniert zwar, aber das Verhalten ist als System nicht wirklich überprüfbar. Es muss mental ausgeführt werden. Noch wichtiger ist, dass eine Änderung eine Beteiligung der Ingenieure erfordert. Sogar eine kleine Optimierung, wie das Anpassen, wann der Überprüfungsschritt angezeigt wird, bedeutet, die Komponente zu bearbeiten, die Validierung zu aktualisieren, eine Pull-Anfrage zu öffnen, auf die Überprüfung zu warten und sie erneut bereitzustellen. Teil 2: Schemagesteuert (SurveyJS) Lassen Sie uns nun denselben Ablauf mithilfe eines Schemas erstellen. Installation npm install Survey-Core Survey-React-UI @tanstack/react-query
Survey-Core: Die vom MIT lizenzierte plattformunabhängige Laufzeit-Engine, die das Formularrendering von SurveyJS unterstützt – der Teil, der uns hier am Herzen liegt. Es verwendet ein JSON-Schema, erstellt daraus ein internes Modell und verarbeitet alles, was sonst in Ihrer React-Komponente enthalten wäre: Bewertung von Sichtbarkeitsausdrücken, Berechnung abgeleiteter Werte, Verwaltung des Seitenstatus, Verfolgung der Validierung und Entscheidung, was „vollständig“ bedeutet, wenn man bedenkt, welche Seiten tatsächlich angezeigt wurden.
Survey-react-uiDie UI-/Rendering-Ebene, die dieses Modell mit React verbindet. Es handelt sich im Wesentlichen um eine
Zusammen bieten sie Ihnen eine voll funktionsfähige, mehrseitige Formularlaufzeit, ohne eine einzige Zeile Kontrollfluss schreiben zu müssen. Das Schemaformat selbst ist, wie bereits erwähnt, nur ein JSON – kein DSL oder irgendetwas Proprietäres. Sie können es einbinden, aus einer Datei importieren, von einer API abrufen oder in einer Datenbankspalte speichern und zur Laufzeit hydrieren. Die gleiche Form wie Daten Hier ist das gleiche Formular, dieses Mal ausgedrückt als JSON-Objekt. Das Schema definiert alles: Struktur, Validierung, Sichtbarkeitsregeln, abgeleitete Berechnungen, Seitennavigation – und übergibt es an ein Modell, das es zur Laufzeit auswertet. So sieht das vollständig aus:
export const SurveySchema = { title: "Order Flow", showProgressBar: "top", seiten: [ { name: "details", elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true, validators: [{ type: "email", text: "Invalid email" }] } ] }, { name: "order", Elemente: [ { Typ: „Text“, Name: „Preis“, Eingabetyp: „Zahl“, Standardwert: 0 }, { Typ: „Text“, Name: „Menge“, Eingabetyp: „Zahl“, Standardwert: 1 }, { Typ: „Dropdown“,Name: „taxRate“, Standardwert: 0,1, Auswahlmöglichkeiten: [ { Wert: 0,05, Text: „5 %“ }, { Wert: 0,1, Text: „10 %“ }, { Wert: 0,15, Text: „15 %“ } ] }, { Typ: „Ausdruck“, Name: „Zwischensumme“, Ausdruck: „{Preis} {Menge}“ }, { Typ: „Ausdruck“, Name: "tax", Ausdruck: "{subtotal} {taxRate}" }, { Typ: "expression", Name: "total", Ausdruck: "{subtotal} + {tax}" } ] }, { name: "account", elements: [ { type: "radiogroup", name: "hasAccount", choice: ["Yes", "No"] }, { type: "text", name: "username", sichtbarIf: "{hasAccount} = 'Yes'", isRequired: true }, { type: "text", name: "password", inputType: "password", sichtbarIf: "{hasAccount} = 'Yes'", isRequired: true, validators: [{ type: "text", minLength: 6, text: "Min. 6 Zeichen" }] }, { type: "rating", name: "satisfaction", rateMin: 1, rateMax: 5 }, { type: "comment", name: "positiveFeedback", sichtbarIf: "{Zufriedenheit} >= 4" }, { Typ: "Kommentar", Name: "improvementFeedback", sichtbarIf: "{Zufriedenheit} <= 2" } ] }, { Name: "Bewertung", sichtbarIf: "{Gesamt} >= 100", Elemente: [] } ]};
Vergleichen Sie dies für einen Moment mit der RHF-Version.
Der superRefine-Block, der bedingt einen Benutzernamen und ein Passwort erforderte, ist verschwunden. VisibleIf: „{hasAccount} = ‚Yes‘“ in Kombination mit isRequired: true behandelt beide Anliegen gemeinsam im Feld selbst, wo Sie sie erwarten würden. Die useWatch + useMemo-Kette, die Zwischensumme, Steuer und Gesamtsumme berechnet hat, wird durch drei Ausdrucksfelder ersetzt, die namentlich aufeinander verweisen. Der Zustand der Überprüfungsseite, der in der RHF-Version nur durch Nachverfolgung über showSubmit, den Schritt-3-Renderzweig, rekonstruierbar war. Und schließlich ist die Navigationsschaltflächenlogik eine einzelne sichtbare If-Eigenschaft im Seitenobjekt.
Die gleiche Logik ist da. Es ist nur so, dass das Schema ihm einen Ort zum Leben gibt, an dem es isoliert sichtbar ist, anstatt über die Komponente verteilt zu sein. Beachten Sie außerdem, dass das Schema den Typ „Ausdruck“ für Zwischensumme, Steuer und Gesamtsumme verwendet. Der Ausdruck ist schreibgeschützt und wird hauptsächlich zur Anzeige berechneter Werte verwendet. SurveyJS unterstützt auch den Typ „html“ für statische Inhalte, für berechnete Werte ist jedoch Ausdruck die richtige Wahl. Nun zur React-Seite. Rendering und Einreichung Ganz einfach. Verknüpfen Sie onComplete auf die gleiche Weise mit Ihrer API – über useMutation oder Plain Fetch:
import { useState, useEffect, useRef } from „react“;import { useMutation } from „@tanstack/react-query“;import { Model } from „survey-core“;import { Survey } from „survey-react-ui“;import „survey-core/survey-core.css“;
export function SurveyForm() { const [model] = useState(() => new Model(surveySchema));
const mutation = useMutation({ mutationFn: async (data) => { const res = waiting fetch("/api/orders", { Methode: „POST“, Header: { "Content-Type": "application/json" }, Körper: JSON.stringify(data), }); if (!res.ok) throw new Error("Fehler beim Senden"); return res.json(); }, });
const mutationRef = useRef(mutation); mutationRef.current = Mutation; useEffect(() => { const handler = (sender) => mutationRef.current.mutate(sender.data); model.onComplete.add(handler); return () => model.onComplete.remove(handler); }, [model]); // ref vermeidet die Neuregistrierung des Handlers bei jedem Rendern (Änderungen der Mutationsobjektidentität)
zurück (
<>
Siehe den Pen SurveyJS-03-SurveyJS [forked] von sixxtinction.
onComplete wird ausgelöst, wenn der Benutzer das Ende der letzten sichtbaren Seite erreicht. Wenn die Gesamtsumme also nie 100 überschreitet und die Überprüfungsseite übersprungen wird, wird sie trotzdem korrekt ausgelöst, da SurveyJS die Sichtbarkeit bewertet, bevor entschieden wird, was „letzte Seite“ bedeutet. Dann enthält sender.data alle Antworten zusammen mit den berechneten Werten (Zwischensumme, Steuer, Gesamt) als erstklassige Felder, sodass die API-Nutzlast mit der identisch ist, die die RHF-Version manuell in onSubmit zusammengestellt hat. DerDas mutationRef-Muster ist dasselbe, nach dem Sie überall dort greifen würden, wo Sie einen stabilen Ereignishandler über einen Wert benötigen, der sich bei jedem Rendern ändert – es ist nichts SurveyJS-spezifisches daran.
Die React-Komponente enthält überhaupt keine Geschäftslogik mehr. Es gibt kein useWatch, kein bedingtes JSX, keinen Schrittzähler, keine useMemo-Kette, kein superRefine. React macht das, was es eigentlich gut kann: eine Komponente rendern und mit einem API-Aufruf verbinden. Was ist aus der Reaktion geraten?
Sorge RHF-Stack SurveyJS Sichtbarkeit JSX-Zweige sichtbarWenn Abgeleitete Werte useWatch / useMemo Ausdruck Feldübergreifende Regeln superRefine Schemabedingungen Navigation Schrittzustand Seite sichtbarWenn Regelstandort Auf Dateien verteilt Zentralisiert im Schema
Was in React verbleibt, sind Layout, Stil, Einreichungsverkabelung und App-Integration, also die Dinge, für die React eigentlich entwickelt wurde. Alles andere wurde in das Schema verschoben, und da das Schema nur ein JSON-Objekt ist, kann es in einer Datenbank gespeichert, unabhängig von Ihrem Anwendungscode versioniert oder mit internen Tools bearbeitet werden, ohne dass eine Bereitstellung erforderlich ist. Ein Produktmanager, der den Schwellenwert ändern muss, der die Überprüfungsseite auslöst, kann dies tun, ohne die Komponente zu berühren. Dies ist ein bedeutender betrieblicher Unterschied für Teams, in denen sich das Formularverhalten häufig weiterentwickelt und nicht immer von Ingenieuren gesteuert wird. Wann sollte jeder Ansatz verwendet werden? Hier ist eine gute Faustregel, die für mich funktioniert: Stellen Sie sich vor, Sie löschen das Formular vollständig. Was würdest du verlieren?
Wenn es sich um Bildschirme handelt, benötigen Sie komponentengesteuerte Formulare. Wenn es Geschäftslogik wie Schwellenwerte, Verzweigungsregeln und bedingte Anforderungen ist, die echte Entscheidungen kodieren, benötigen Sie eine Schema-Engine.
Auch wenn es bei den bevorstehenden Änderungen hauptsächlich um Beschriftungen, Felder und Layout geht, ist RHF genau das Richtige für Sie. Wenn es um Bedingungen, Ergebnisse und Regeln geht, die Ihr Betriebs- oder Rechtsteam möglicherweise an einem Dienstagnachmittag anpassen muss, ohne ein Ticket einzureichen, ist das Schemamodell mit SurveyJS die ehrlichere Lösung. Diese beiden Ansätze stehen nicht wirklich in Konkurrenz zueinander. Sie befassen sich mit verschiedenen Problemklassen, und der Fehler, den es zu vermeiden gilt, besteht darin, die Abstraktion nicht mit dem Gewicht der Logik in Einklang zu bringen – ein Regelsystem wie eine Komponente zu behandeln, weil es das vertraute Werkzeug ist, oder nach einer Richtlinien-Engine zu greifen, weil ein Formular auf drei Schritte angewachsen ist und ein bedingtes Feld erhalten hat. Die Form, die wir hier erstellt haben, liegt bewusst nahe an der Grenze und ist komplex genug, um den Unterschied deutlich zu machen, aber nicht so extrem, dass der Vergleich manipuliert wirkt. Die meisten realen Formen, die in Ihrer Codebasis unhandlich geworden sind, befinden sich wahrscheinlich in der Nähe derselben Grenze, und die Frage ist normalerweise nur, ob jemand benannt hat, was sie tatsächlich sind. Verwenden Sie React Hook Form + Zod, wenn:
Formulare sind CRUD-orientiert; Die Logik ist oberflächlich und UI-gesteuert. Ingenieure besitzen jedes Verhalten; Das Backend bleibt die Quelle der Wahrheit.
Verwenden Sie SurveyJS, wenn:
Formulare verschlüsseln Geschäftsentscheidungen; Regeln entwickeln sich unabhängig von der Benutzeroberfläche; Die Logik muss sichtbar, überprüfbar oder versioniert sein. Nicht-Ingenieure beeinflussen das Verhalten; Das gleiche Formular muss über mehrere Frontends laufen.