Aquest article està patrocinat per SurveyJS Hi ha un model mental que la majoria dels desenvolupadors de React comparteixen sense parlar-ne mai en veu alta. Que les formes sempre han de ser components. Això significa una pila com:
Formulari React Hook per a l'estat local (representacions mínimes, registre de camp ergonòmic, interacció imperativa). Zod per a la validació (correcció d'entrada, validació de límits, anàlisi de tipus segur). React Query per al backend: enviament, reintents, memòria cau, sincronització del servidor, etc.
I per a la gran majoria de formularis: les vostres pantalles d'inici de sessió, les vostres pàgines de configuració, els vostres mods CRUD, això funciona molt bé. Cada peça fa la seva feina, es componen de manera neta i podeu passar a les parts de la vostra aplicació que realment diferencien el vostre producte. Però de tant en tant, un formulari comença a acumular coses com ara regles de visibilitat que depenen de respostes anteriors, o valors derivats que passen en cascada a través de tres camps. Potser fins i tot pàgines senceres que s'han de saltar o mostrar en funció d'un total acumulat. Gestioneu el primer condicional amb un useWatch i una branca en línia, que està bé. Després un altre. Aleshores esteu arribant a SuperRefine per codificar regles de camp creuat que el vostre esquema Zod no pot expressar de la manera normal. Aleshores, la navegació per passos comença a filtrar la lògica empresarial. En algun moment, observeu el que heu creat i us adoneu que el formulari ja no és realment una interfície d'usuari. És més aviat un procés de decisió i l'arbre de components és just on l'heu d'emmagatzemar. Aquí és on crec que el model mental de les formes a React es trenca, i realment no és culpa de ningú. La pila RHF + Zod és excel·lent pel que va ser dissenyada. El problema és que tendim a seguir utilitzant-lo més enllà del punt en què les seves abstraccions coincideixen amb el problema perquè l'alternativa requereix una manera totalment diferent de pensar les formes. Aquest article tracta sobre aquesta alternativa. Per mostrar-ho, construirem exactament el mateix formulari de diversos passos dues vegades:
Amb el formulari React Hook + Zod connectat a React Query per enviar-lo, Amb SurveyJS, que tracta un formulari com a dades (un simple esquema JSON) en lloc d'un arbre de components.
Els mateixos requisits, la mateixa lògica condicional, la mateixa trucada d'API al final. A continuació, mapejarem exactament què es va moure i què es va quedar, i establirem una manera pràctica de decidir quin model heu d'utilitzar i quan. El formulari que estem construint:
Aquest formulari utilitzarà un flux de 4 passos: Pas 1: Detalls
Nom (obligatori), Correu electrònic (obligatori, format vàlid).
Pas 2: Comanda
Preu unitari, quantitat, tipus impositiu, Derivat: Subtotal, impostos, Total.
Pas 3: compte i comentaris
Tens un compte? (Sí/No) Si sí → nom d'usuari + contrasenya, tots dos són obligatoris. Si No → el correu electrònic ja s'ha recollit al pas 1.
Grau de satisfacció (1–5) Si ≥ 4 → pregunta "Què t'ha agradat?" Si ≤ 2 → pregunta "Què podem millorar?"
Pas 4: Revisió
Només apareix si el total és >= 100 Presentació final.
Això no és extrem. Però n'hi ha prou per exposar les diferències arquitectòniques. Part 1: impulsat per components (Forma de ganxo de reacció + Zod) Instal·lació npm instal·la react-hook-form zod @hookform/resolvers @tanstack/react-query
Esquema Zod Comencem amb l'esquema Zod, perquè normalment és on s'estableix la forma de la forma. Per als dos primers passos, dades personals i entrades de comanda, tot és senzill: cadenes necessàries, números amb mínims i una enumeració. La part interessant comença quan intentes expressar les regles condicionals.
importa {z} de "zod";
export const formSchema = z.object({ firstName: z.string().min(1, "Obligatori"), correu electrònic: z.string().email("Correu electrònic no vàlid"), preu: z.number().min(0), quantitat: z.number().min(1), taxRate: z.number(), hasAccount: "["]), username: z.string().optional(), contrasenya: z.string().optional(), satisfacció: z.number().min(1).max(5), positiveFeedback: z.string().optional(), improvementFeedback: z.string().optional(),}).superRefine((data, ctx) => { if (data, ctx) => { if (data, ctx) => { if (data, ctx) => {) === "! ctx.addIssue({ codi: "personalitzat", ruta: ["nom d'usuari"], missatge: "Obligatori" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ codi: "personalitzat", camí: ["contrasenya"], missatge: "Mínim 6 caràcters}"});
if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ codi: "custom", ruta: ["positiveFeedback"], missatge: "Si us plau comparteix el que t'agrada" }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ codi: "personalitzat", ruta:["improvementFeedback"], missatge: "Si us plau, digueu-nos què hem de millorar"}); }});
tipus d'exportació FormData = z.infer
Tingueu en compte que el nom d'usuari i la contrasenya s'escriuen com a opcionals () tot i que són necessaris condicionalment perquè l'esquema de nivell de tipus de Zod descriu la forma de l'objecte, no les regles que regeixen quan els camps són importants. El requisit condicional ha de viure dins de superRefine, que s'executa després de validar la forma i té accés a l'objecte complet. Aquesta separació no és un defecte; és només per a què està dissenyada l'eina: superRefine és on va la lògica de camp creuat quan no es pot expressar a l'estructura de l'esquema. El que també és notable aquí és el que aquest esquema no expressa. No té cap concepte de pàgines, cap concepte de quins camps són visibles en quin punt, ni concepte de navegació. Tot això viurà en un altre lloc. Component del formulari
importar { useForm, useWatch } de "react-hook-form";importar { zodResolver } de "@hookform/resolvers/zod";importar { useMutation } des de "@tanstack/react-query";importar { useState, useMemo } de "react";importar {formSchema, tipus Form./schema};
const STEPS = ["detalls", "comanda", "compte", "revisió"];
tipus OrderPayload = FormData & { subtotal: nombre; impost: número; total: nombre };
funció d'exportació RHFMultiStepForm() { const [step, setStep] = useState(0);
const mutation = useMutation ({ mutationFn: async (càrrega útil: OrderPayload) => { const res = await fetch ("/api/comandes", { mètode: "POST", capçaleres: { "Content-Type": "application/json" }, cos: JSON.stringify (càrrega útil), }); if (!res.ok) throw new Error("No s'ha pogut enviar"); retorna res.json(); }, });
const { register, control, handleSubmit, formState: { errors }, } = useForm
retorn (
);}Vegeu el Pen SurveyJS-03-RHF [bifurcat] per sixthextinction. Aquí estan passant moltes coses, i val la pena frenar per notar on van acabar les coses.
Els valors derivats (subtotal, impost, total) es calculen al component mitjançant useWatch i useMemo perquè depenen dels valors del camp en directe i no hi ha cap altre lloc natural per a ells. Les regles de visibilitat per al nom d'usuari, la contrasenya, els comentaris positius i els comentaris de millora viuen a JSX com a condicionals en línia. La lògica de salt de passos (la pàgina de revisió només apareix quan el total >= 100) està incrustada a la variable showSubmit i la condició de renderització del pas 3. La navegació en si és només un comptador useState que estem incrementant manualment. React Query gestiona els reintents, la memòria cau i la invalidació. El formulari només crida a mutation.mutate amb dades validades.
Res d'això està malament, per se. Això encara és un React idiomàtic i el component és bastant rendible gràcies a com RHF aïlla les restitucions. Però si l'haguessis de lliurar a algú que no l'hagués escrit i li demanés que expliqués en quines condicions apareix la pàgina de revisió, haurien de rastrejar a través de showSubmit, la condició de renderització del pas 3 i la lògica del botó de navegació (tres llocs separats) per reconstruir una regla que es podria haver indicat en una línia. La forma funciona, sí, però el comportament no és realment inspeccionable com a sistema. S'ha d'executar mentalment. Més important encara, canviar-lo requereix la participació de l'enginyeria. Fins i tot un petit ajust, com ara ajustar quan apareix el pas de revisió, significa editar el component, actualitzar la validació, obrir una sol·licitud d'extracció, esperar la revisió i tornar a desplegar-lo. Part 2: basat en esquemes (SurveyJS) Ara construïm el mateix flux utilitzant un esquema. Instal·lació npm install survey-core survey-react-ui @tanstack/react-query
survey-coreEl motor d'execució independent de la plataforma amb llicència del MIT que impulsa la representació de formularis de SurveyJS, la part que ens importa aquí. Pren un esquema JSON, construeix un model intern a partir d'ell i gestiona tot el que d'altra manera hi hauria al vostre component React: avaluar expressions de visibilitat, calcular valors derivats, gestionar l'estat de la pàgina, fer el seguiment de la validació i decidir què significa "complet" tenint en compte quines pàgines es van mostrar realment.
survey-react-uiLa capa d'interfície d'usuari / renderització que connecta aquest model amb React. Es tracta bàsicament d'un component
En conjunt, us ofereixen un temps d'execució de formularis de diverses pàgines totalment funcional sense escriure una sola línia de flux de control. El format de l'esquema en si és, com s'ha dit abans, només un JSON, sense DSL ni res propietari. Podeu integrar-lo, importar-lo d'un fitxer, obtenir-lo d'una API o emmagatzemar-lo en una columna de base de dades i hidratar-lo en temps d'execució. La mateixa forma, com les dades Aquí teniu la mateixa forma, aquesta vegada expressada com a objecte JSON. L'esquema ho defineix tot: estructura, validació, regles de visibilitat, càlculs derivats, navegació per pàgines i el lliura a un model que l'avalua en temps d'execució. Aquí teniu l'aspecte complet:
export const surveySchema = { title: "Order Flow", showProgressBar: "top", pàgines: [ { name: "details", elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true, validators: [{} type: "{correu electrònic vàlid"] nom: "ordre", elements: [ { tipus: "text", nom: "preu", inputType: "nombre", defaultValue: 0 }, { type: "text", nom: "quantity", inputType: "number", defaultValue: 1 }, { type: "dropdown",nom: "taxRate", valor predeterminat: 0,1, opcions: [ { valor: 0,05, text: "5%" }, { valor: 0,1, text: "10%" }, { valor: 0,15, text: "15%" } ] }, { tipus: "expressió", nom: "subtotal", expressió: {quantitat: "{"preu"} expressió, {quantitat: {tipo:}"} expressió: "impost", expressió: "{subtotal} {taxRate}" }, { tipus: "expressió", nom: "total", expressió: "{subtotal} + {impost}" } ] }, { nom: "compte", elements: [ { tipus: "radiogroup", nom: "hasAccount", opcions: ["Sí", "No"] }, nom: "nom"] }, nom: "feu nom:" "{hasAccount} = 'Sí'", isRequired: true }, { type: "text", name: "password", inputType: "password", visibleIf: "{hasAccount} = 'Yes'", isRequired: true, validators: [{ type: "text", minLength: 6, text: "Min 6 characters}," {}: ", accionaments, name rateMin: 1, rateMax: 5 }, { tipus: "comentari", nom: "positiveFeedback", visibleIf: "{satisfacció} >= 4" }, { tipus: "comment", nom: "improvementFeedback", visibleIf: "{satisfacció} <= 2" } ] }, {name: ": "review", {name: ": "review", {name: ": "review", {nom: ": "{0} elements visibles" [] } ]};
Compareu-ho un moment amb la versió RHF.
El bloc superRefine que requeria condicionalment el nom d'usuari i la contrasenya ha desaparegut. visibleIf: "{hasAccount} = 'Sí'" combinat amb isRequired: true gestiona ambdues inquietuds conjuntament, al camp mateix, on espereu trobar-les. La cadena useWatch + useMemo que calculava el subtotal, l'impost i el total es substitueix per tres camps d'expressió que es refereixen entre si pel nom. La condició de la pàgina de revisió, que a la versió RHF només es podia reconstruir rastrejant a través de showSubmit, la branca de renderització del pas 3. I, finalment, la lògica del botó de navegació és una única propietat visibleIf a l'objecte de la pàgina.
Hi ha la mateixa lògica. És només que l'esquema li dóna un lloc on viure on és visible de manera aïllada, en lloc de repartir-se per tot el component. A més, tingueu en compte que l'esquema utilitza el tipus: "expressió" per al subtotal, l'impost i el total. L'expressió és només de lectura i s'utilitza principalment per mostrar valors calculats. SurveyJS també admet el tipus: "html" per a contingut estàtic, però per als valors calculats, l'expressió és l'opció correcta. Ara pel costat de React. Renderització i tramesa Molt senzill. Connecteu onComplete a la vostra API de la mateixa manera: mitjançant useMutation o simple fetch:
importar { useState, useEffect, useRef } de "reaccionar";importar { useMutation } de "@tanstack/react-query";importar { Model } de "survey-core";importar { Survey } de "survey-react-ui";importar "survey-core/survey-core.css";
export function SurveyForm() { const [model] = useState(() => new Model(surveySchema));
const mutation = useMutation ({ mutationFn: async (dades) => { const res = await fetch ("/api/comandes", { mètode: "POST", capçaleres: { "Content-Type": "application/json" }, cos: JSON.stringify(dades), }); if (!res.ok) throw new Error("No s'ha pogut enviar"); retorna res.json(); }, });
const mutationRef = useRef(mutation); mutationRef.current = mutació; useEffect(() => { const handler = (sender) => mutationRef.current.mutate (sender.data); model.onComplete.add (handler); return () => model.onComplete.remove (handler); }, [model]); // ref evita tornar a registrar el controlador cada renderització (canvis d'identitat de l'objecte de mutació)
tornar (
<>
Vegeu el Pen SurveyJS-03-SurveyJS [forked] per sixthextinction.
onComplete s'activa quan l'usuari arriba al final de l'última pàgina visible. Per tant, si el total mai no supera els 100 i la pàgina de revisió s'omet, encara s'activa correctament perquè SurveyJS avalua la visibilitat abans de decidir què significa "última pàgina". Aleshores, sender.data conté totes les respostes juntament amb els valors calculats (subtotal, impost, total) com a camps de primera classe, de manera que la càrrega útil de l'API és idèntica a la que la versió RHF va muntar manualment a onSubmit. ElEl patró mutationRef és el mateix al qual arribaríeu a qualsevol lloc on necessiteu un controlador d'esdeveniments estable sobre un valor que canvia a cada renderització, res específic de SurveyJS al respecte.
El component React ja no conté cap lògica empresarial. No hi ha useWatch, ni JSX condicional, ni comptador de passos, ni cadena useMemo, ni superRefine. React està fent el que realment és bo: renderitzar un component i connectar-lo a una trucada d'API. Què va sortir de reaccionar?
Preocupació Pila RHF EnquestaJS Visibilitat Sucursals de JSX visibleSi Valors derivats useWatch / useMemo expressió Regles entre camps superrefinar Condicions de l'esquema Navegació estat de pas Pàgina visibleSi Localització de la regla Distribuït entre fitxers Centralitzat en l'esquema
El que es queda a React és el disseny, l'estil, el cablejat d'enviament i la integració d'aplicacions, és a dir, les coses per a les quals està dissenyat React. Tota la resta s'ha mogut a l'esquema i, com que l'esquema és només un objecte JSON, es pot emmagatzemar en una base de dades, versionar-lo independentment del codi de l'aplicació o editar-lo mitjançant eines internes sense necessitat d'un desplegament. Un gestor de producte que ha de canviar el llindar que activa la pàgina de revisió pot fer-ho sense tocar el component. Aquesta és una diferència operativa significativa per als equips on el comportament del formulari evoluciona amb freqüència i no sempre és impulsat pels enginyers. Quan utilitzar cada enfocament? Aquí hi ha una bona regla general que funciona per a mi: imagineu-vos que suprimiu el formulari completament. Què perdries?
Si es tracta de pantalles, voleu formularis basats en components. Si es tracta de lògica empresarial, com ara llindars, regles de ramificació i requisits condicionals que codifiquen decisions reals, voleu un motor d'esquemes.
De la mateixa manera, si els canvis que us arriben són principalment sobre etiquetes, camps i disseny, RHF us servirà bé. Si es tracta de condicions, resultats i regles que el vostre equip operatiu o legal podria haver d'ajustar un dimarts a la tarda sense presentar un bitllet, el model d'esquema amb SurveyJS és el més honest. Aquests dos enfocaments no estan realment en competència entre ells. Aborden diferents classes de problemes, i l'error que val la pena evitar és no fer coincidir l'abstracció amb el pes de la lògica: tractar un sistema de regles com un component perquè aquesta és l'eina familiar, o cercar un motor de polítiques perquè un formulari va créixer a tres passos i va adquirir un camp condicional. La forma que hem construït aquí es troba a prop del límit deliberadament, prou complexa per exposar la diferència, però no tan extrema que la comparació sembli manipulada. La majoria de les formes reals que s'han tornat difícils de manejar a la vostra base de codi probablement se situen a prop d'aquest mateix límit, i la pregunta sol ser si algú ha nomenat com són realment. Utilitzeu React Hook Form + Zod quan:
Els formularis estan orientats a CRUD; La lògica és poc profunda i es basa en la interfície d'usuari; Els enginyers posseeixen tot el comportament; El backend continua sent la font de la veritat.
Utilitzeu SurveyJS quan:
Els formularis codifiquen decisions empresarials; Les regles evolucionen independentment de la IU; La lògica ha de ser visible, auditable o versionada; Els no enginyers influeixen en el comportament; El mateix formulari s'ha d'executar a través de múltiples interfícies.