Este artigo está patrocinado por SurveyJS Hai un modelo mental que a maioría dos desenvolvedores de React comparten sen discutilo nunca en voz alta. Que as formas sempre deben ser compoñentes. Isto significa unha pila como:
Formulario React Hook para o estado local (representacións mínimas, rexistro de campo ergonómico, interacción imperativa). Zod para validación (corrección de entrada, validación de límites, análise de tipo seguro). React Query para o backend: envío, reintentos, caché, sincronización do servidor, etc.
E para a gran maioría dos formularios: as túas pantallas de inicio de sesión, as túas páxinas de configuración, os teus modais CRUD, isto funciona moi ben. Cada peza fai o seu traballo, compoñen de forma limpa e podes pasar ás partes da túa aplicación que realmente diferencian o teu produto. Pero, de vez en cando, un formulario comeza a acumular cousas como regras de visibilidade que dependen de respostas anteriores ou valores derivados que pasan a través de tres campos. Quizais incluso páxinas enteiras que deberían omitirse ou mostrarse en función dun total acumulado. Manexa o primeiro condicional cun useWatch e unha rama en liña, o que está ben. Despois outra. Entón estás a buscar superRefine para codificar regras de campo cruzado que o teu esquema Zod non pode expresar da forma normal. Entón, a navegación por pasos comeza a filtrar a lóxica empresarial. Nalgún momento, mira o que construíu e dáse conta de que o formulario xa non é realmente unha interface de usuario. É máis un proceso de decisión, e a árbore de compoñentes é só onde o almacenaches. Aquí é onde creo que o modelo mental para as formas en React rompe, e realmente non é culpa de ninguén. A pila RHF + Zod é excelente para o que foi deseñada. O problema é que tendemos a seguir usándoo máis aló do punto en que as súas abstraccións coinciden co problema porque a alternativa require unha forma totalmente diferente de pensar as formas. Este artigo trata sobre esa alternativa. Para mostrar isto, construiremos exactamente o mesmo formulario de varios pasos dúas veces:
Con React Hook Form + Zod conectado a React Query para o envío, Con SurveyJS, que trata un formulario como datos (un esquema JSON simple) en lugar de unha árbore de compoñentes.
Mesmos requisitos, mesma lóxica condicional, mesma chamada API ao final. A continuación, mapearemos exactamente o que se moveu e o que quedou, e presentaremos unha forma práctica de decidir que modelo debes usar e cando. O formulario que estamos a construír:
Este formulario utilizará un fluxo de 4 pasos: Paso 1: Detalles
Nome (obrigatorio), Correo electrónico (obrigatorio, formato válido).
Paso 2: Orde
Prezo unitario, Cantidade, Tipo impositivo, Derivado: Subtotal, imposto, Total.
Paso 3: conta e comentarios
Tes unha conta? (Si/Non) Se si → nome de usuario + contrasinal, ambos son necesarios. Se Non → correo electrónico xa recompilado no paso 1.
Clasificación de satisfacción (1–5) Se ≥ 4 → pregunta "Que che gustou?" Se ≤ 2 → pregunta "Que podemos mellorar?"
Paso 4: Revisión
Só aparece se total >= 100 Presentación final.
Isto non é extremo. Pero é suficiente para expoñer as diferenzas arquitectónicas. Parte 1: Controlado por compoñentes (Forma de gancho de reacción + Zod) Instalación npm instalar react-hook-form zod @hookform/resolvers @tanstack/react-query
Esquema Zod Comecemos co esquema Zod, porque normalmente é onde se establece a forma da forma. Para os dous primeiros pasos, datos persoais e entradas de pedido, todo é sinxelo: cadeas necesarias, números con mínimos e unha enumeración. A parte interesante comeza cando intentas expresar as regras condicionais.
importar {z} de "zod";
export const formSchema = z.object({ firstName: z.string().min(1, "Requirido"), correo electrónico: z.string().email("Correo electrónico non válido"), prezo: z.number().min(0), cantidade: z.number().min(1), taxRate: z.number(), hasAccount: "["]), username: z.string().optional(), contrasinal: z.string().optional(), satisfacción: z.number().min(1).max(5), positiveFeedback: z.string().optional(), improvementFeedback: z.string().optional(),}).superRefine((datos, ctx) => { if (data, ctx) => { if (data, ctx) => { if (data, ctx) => { if (data, ctx) => { if (datos) === "! ctx.addIssue({ código: "personalizado", ruta: ["nome de usuario"], mensaxe: "Obrigatorio" }); } if (!data.password || data.password.length < 6) { ctx.addIssue({ código: "personalizado", ruta: ["contrasinal"], mensaxe: "Mín 6 caracteres}"});
if (data.satisfaction >= 4 && !data.positiveFeedback) { ctx.addIssue({ código: "personalizado", ruta: ["positiveFeedback"], mensaxe: "Comparte o que che gustou" }); }
if (data.satisfaction <= 2 && !data.improvementFeedback) { ctx.addIssue({ código: "personalizado", ruta:["improvementFeedback"], mensaxe: "Por favor, díganos o que debemos mellorar"}); }});
tipo de exportación FormData = z.infer
Teña en conta que o nome de usuario e o contrasinal escríbense como opcional() aínda que son obrigatorios condicionalmente porque o esquema de nivel de tipo de Zod describe a forma do obxecto, non as regras que rexen cando importan os campos. O requisito condicional ten que vivir dentro de superRefine, que se executa despois de que se valida a forma e ten acceso ao obxecto completo. Esa separación non é un defecto; é só para o que está deseñada a ferramenta: superRefine é onde vai a lóxica de campo cruzado cando non se pode expresar na propia estrutura do esquema. O que tamén é notable aquí é o que este esquema non expresa. Non ten concepto de páxinas, ningún concepto de que campos son visibles en que punto e ningún concepto de navegación. Todo iso vivirá noutro lugar. Compoñente do formulario
importar { useForm, useWatch } de "react-hook-form";importar { zodResolver } de "@hookform/resolvers/zod";importar { useMutation } de "@tanstack/react-query";importar { useState, useMemo } de "react";importar { formSchema, tipo "FormDataschema"};
const STEPS = ["detalles", "pedido", "conta", "revisión"];
tipo OrderPayload = FormData & { subtotal: número; imposto: número; total: número };
función de exportación RHFMultiStepForm() { const [paso, setStep] = useState(0);
mutación constante = useMutation({ mutationFn: async (carga útil: OrderPayload) => { const res = agardar fech ("/api/ordes", { método: "POST", cabeceiras: { "Content-Type": "application/json" }, corpo: JSON.stringify(carga útil), }); if (!res.ok) throw new Error("Produciuse un erro ao enviar"); devolver res.json(); }, });
const { register, control, handleSubmit, formState: { erros }, } = useForm
return (
);}Consulte o Pen SurveyJS-03-RHF [bifurcado] por sixthextinction. Aquí están pasando moitas cousas, e paga a pena frear o ritmo para notar onde acabaron as cousas.
Os valores derivados (subtotal, imposto, total) calcúlanse no compoñente mediante useWatch e useMemo porque dependen dos valores de campo en directo e non hai outro lugar natural para eles. As regras de visibilidade para o nome de usuario, o contrasinal, os comentarios positivos e os comentarios de mellora están dispoñibles en JSX como condicionais en liña. A lóxica de salto de pasos (a páxina de revisión só aparece cando o total >= 100) está incrustada na variable showSubmit e na condición de renderizado no paso 3. A navegación en si é só un contador useState que estamos incrementando manualmente. React Query xestiona reintentos, caché e invalidación. O formulario só chama mutation.mutate con datos validados.
Nada disto está mal, per se. Este aínda é React idiomático e o compoñente é bastante eficiente grazas a como RHF illada as renderizacións. Pero se lle entregases isto a alguén que non o escribira e lle pedises que explicasen en que condicións aparece a páxina de revisión, terían que rastrexar a través de showSubmit, a condición de renderización do paso 3 e a lóxica do botón de navegación (tres lugares separados) para reconstruír unha regra que se puidese indicar nunha soa liña. A forma funciona, si, pero o comportamento non é realmente inspeccionable como sistema. Hai que executalo mentalmente. Máis importante aínda, cambialo require a participación da enxeñería. Incluso un pequeno axuste, como axustar cando aparece o paso de revisión, significa editar o compoñente, actualizar a validación, abrir unha solicitude de extracción, esperar a revisión e implementar de novo. Parte 2: Controlado por esquemas (SurveyJS) Agora imos construír o mesmo fluxo usando un esquema. Instalación npm install survey-core survey-react-ui @tanstack/react-query
survey-coreO motor de execución independente da plataforma con licenza do MIT que potencia a representación de formularios de SurveyJS, a parte que nos importa aquí. Leva un esquema JSON, constrúe un modelo interno a partir del e xestiona todo o que doutro xeito existiría no teu compoñente React: avaliar expresións de visibilidade, calcular valores derivados, xestionar o estado da páxina, rastrexar a validación e decidir o que significa "completar" dadas as páxinas que se mostraron realmente.
survey-react-uiA capa de renderizado/UI que conecta ese modelo con React. É esencialmente un compoñente
Xuntos, ofrécenlle un tempo de execución de formularios de varias páxinas totalmente funcional sen escribir unha soa liña de fluxo de control. O formato do esquema en si é, como se dixo antes, só un JSON, sen DSL nin nada propietario. Podes incorporalo, importalo desde un ficheiro, recuperalo dunha API ou almacenalo nunha columna de base de datos e hidratalo no tempo de execución. A mesma forma, como datos Aquí está a mesma forma, esta vez expresada como un obxecto JSON. O esquema defíneo todo: estrutura, validación, regras de visibilidade, cálculos derivados, navegación por páxinas e entrégao a un modelo que o avalía en tempo de execución. Aquí tes o que parece completo:
export const surveySchema = { title: "Order Flow", showProgressBar: "top", pages: [ { name: "details", elements: [ { type: "text", name: "firstName", isRequired: true }, { type: "text", name: "email", inputType: "email", isRequired: true, validators: [{ type: "], valides: "text" nome: "orde", elementos: [ { type: "text", nome: "price", inputType: "number", defaultValue: 0 }, { type: "text", name: "cantity", inputType: "number", defaultValue: 1 }, { type: "dropdown",nome: "taxRate", valor predeterminado: 0,1, opcións: [ { valor: 0,05, texto: "5%" }, { valor: 0,1, texto: "10%" }, { valor: 0,15, texto: "15%" } ] }, { tipo: "expresión", nome: "subtotal", expresión: {cantidade: "tipo de prezo", expresión: {cantidade: {}"} expresión: "tax", expresión: "{subtotal} {taxRate}" }, { tipo: "expresión", nome: "total", expresión: "{subtotal} + {tax}" } ] }, { nome: "conta", elementos: [ { type: "radiogroup", nome: "hasAccount", opcións: ["Si", "Non"] }, {:"nombre de tipo visible", {::"nombre de texto "{hasAccount} = 'Si'", isRequired: true }, { type: "text", name: "password", inputType: "password", visibleIf: "{hasAccount} = 'Yes'", isRequired: true, validators: [{ type: "text", minLength: 6, text: "Min 6 characters}," {}: ", s. rateMin: 1, rateMax: 5 }, { type: "comentario", nome: "positiveFeedback", visibleIf: "{satisfacción} >= 4" }, { type: "comentario", nome: "improvementFeedback", visibleIf: "{satisfacción} <= 2" } ] }, {name: >: "review", {}, elementos visibles: >: "{0} [] } ]};
Compare isto coa versión RHF por un momento.
O bloque superRefine que requiría condicionalmente o nome de usuario e o contrasinal desapareceu. visibleIf: "{hasAccount} = 'Si'" combinado con isRequired: true manexa ambos problemas xuntos, no propio campo, onde esperarías atopalos. A cadea useWatch + useMemo que calculou o subtotal, o imposto e o total substitúese por tres campos de expresión que se refiren entre si polo seu nome. A condición da páxina de revisión, que na versión de RHF só se podía reconstruír rastrexando a través de showSubmit, a rama de renderización do paso 3. E, finalmente, a lóxica do botón de navegación é unha única propiedade visibleIf no obxecto da páxina.
A mesma lóxica está aí. É só que o esquema dálle un lugar para vivir onde é visible de forma illada, en lugar de espallarse polo compoñente. Ademais, teña en conta que o esquema usa tipo: "expresión" para subtotal, imposto e total. A expresión é só de lectura e úsase principalmente para mostrar valores calculados. SurveyJS tamén admite o tipo: "html" para contido estático, pero para os valores calculados, a expresión é a opción correcta. Agora para o lado React. Presentación e presentación Moi sinxelo. Conecte onComplete á súa API do mesmo xeito: mediante useMutation ou obtención sinxela:
importar { useState, useEffect, useRef } de "react";import { useMutation } de "@tanstack/react-query";importar { Modelo } de "survey-core";importar { Survey } de "survey-react-ui";importar "survey-core/survey-core.css";
export function SurveyForm() { const [modelo] = useState(() => new Model(surveySchema));
mutación constante = useMutation({ mutationFn: async (datos) => { const res = agardar fech ("/api/ordes", { método: "POST", cabeceiras: { "Content-Type": "application/json" }, corpo: JSON.stringify(datos), }); if (!res.ok) throw new Error("Produciuse un erro ao enviar"); devolver res.json(); }, });
const mutationRef = useRef(mutación); mutationRef.current = mutación; useEffect(() => { const handler = (sender) => mutationRef.current.mutate (sender.data); model.onComplete.add (handler); return () => model.onComplete.remove (handler); }, [model]); // ref evita volver rexistrar o controlador cada render (cambios de identidade do obxecto de mutación)
volver (
<>
Consulte o Pen SurveyJS-03-SurveyJS [forked] por sixthextinction.
onComplete dispara cando o usuario chega ao final da última páxina visible. Polo tanto, se o total nunca supera os 100 e se salta a páxina de revisión, aínda se dispara correctamente porque SurveyJS avalía a visibilidade antes de decidir que significa "última páxina". Entón, sender.data contén todas as respostas xunto cos valores calculados (subtotal, imposto, total) como campos de primeira clase, polo que a carga útil da API é idéntica á que a versión de RHF montou manualmente en onSubmit. OO padrón mutationRef é o mesmo ao que chegarías onde necesites un controlador de eventos estable sobre un valor que cambia en cada renderizado; nada específico de SurveyJS.
O compoñente React xa non contén ningunha lóxica empresarial. Non hai useWatch, nin JSX condicional, nin contador de pasos, nin cadea useMemo, nin superRefine. React está facendo o que realmente é bo: renderizar un compoñente e conéctalo a unha chamada API. Que se moveu de reaccionar?
Preocupación Pila RHF Enquisa JS Visibilidade Sucursais JSX visibleSe Valores derivados useWatch / useMemo expresión Regras de campo cruzado superrefinado Condicións do esquema Navegación estado de paso Páxina visibleSe Localización da regra Distribuído en ficheiros Centralizado no esquema
O que queda en React é o deseño, o estilo, o cableado de envío e a integración de aplicacións, é dicir, as cousas para as que realmente está deseñado React. Todo o demais moveuse ao esquema e, como o esquema é só un obxecto JSON, pódese almacenar nunha base de datos, versionarse independentemente do código da súa aplicación ou editarse mediante ferramentas internas sen necesidade de implantación. Un xestor de produto que necesite cambiar o limiar que activa a páxina de revisión pode facelo sen tocar o compoñente. Esa é unha diferenza operativa significativa para os equipos nos que o comportamento do formulario evoluciona con frecuencia e non sempre está dirixido polos enxeñeiros. Cando usar cada enfoque? Aquí tes unha boa regra xeral que me funciona: imaxina eliminar o formulario por completo. Que perderías?
Se se trata de pantallas, quere formularios dirixidos por compoñentes. Se é lóxica empresarial, como limiares, regras de ramificación e requisitos condicionais que codifican decisións reais, quere un motor de esquema.
Do mesmo xeito, se os cambios que se achegan son principalmente etiquetas, campos e deseño, RHF servirache ben. Se se trata de condicións, resultados e regras que o teu equipo legal ou operativo pode ter que axustar un martes pola tarde sen presentar un ticket, o modelo de esquema con SurveyJS é o máis honesto. Estes dous enfoques non están realmente en competencia entre si. Abordan diferentes clases de problemas, e o erro que paga a pena evitar é non axustar a abstracción co peso da lóxica: tratar un sistema de regras como un compoñente porque esa é a ferramenta familiar ou buscar un motor de políticas porque un formulario pasou a tres pasos e adquiriu un campo condicional. A forma que construímos aquí sitúase preto do límite deliberadamente, o suficientemente complexa como para expoñer a diferenza, pero non tan extrema como para que a comparación pareza manipulada. A maioría das formas reais que se volveron difíciles de manexar na túa base de código probablemente se atopen preto dese mesmo límite, e a pregunta adoita ser só se alguén nomeou o que son realmente. Usa React Hook Form + Zod cando:
Os formularios están orientados a CRUD; A lóxica é pouco profunda e está dirixida pola IU; Os enxeñeiros posúen todo o comportamento; O backend segue sendo a fonte da verdade.
Usa SurveyJS cando:
Os formularios codifican decisións comerciais; As regras evolucionan independentemente da IU; A lóxica debe ser visible, auditable ou versionada; Os non enxeñeiros inflúen no comportamento; O mesmo formulario debe executarse en varias interfaces.