Construindo formulários dinâmicos em React e Next.js – Smashing Magazine

PUBLICIDADE

Construindo formulários dinâmicos em React e Next.js - Smashing Magazine

Existe um modelo mental que a maioria dos desenvolvedores do React compartilha sem nunca discuti-lo em voz alta. Que os formulários são sempre deveriam ser componentes. Isso significa uma pilha como:

  • Formulário de gancho de reação para o estado local (re-renderizações mínimas, registro de campo ergonômico, interação imperativa).
  • Zod para validação (correção de entrada, validação de limite, análise de tipo seguro).
  • Consulta de reação para back-end: envio, novas tentativas, armazenamento em cache, sincronização de servidor e assim por diante.

E para a grande maioria dos formulários – suas telas de login, suas páginas de configurações, seus modais CRUD – isso funciona muito bem. Cada peça faz seu trabalho, elas são compostas de forma limpa e você pode passar para as partes de sua aplicação que realmente diferenciam seu produto.

Mas de vez em quando, um formulário começa a acumular coisas como regras de visibilidade que dependem de respostas anteriores ou valores derivados que se espalham por três campos. Talvez até páginas inteiras que deveriam ser ignoradas ou mostradas com base em um total acumulado.

Você lida com a primeira condicional com um useWatch e um branch inline, o que é bom. Depois outro. Então você está alcançando superRefine para codificar regras entre campos que seu esquema Zod não pode expressar da maneira normal. Então, a navegação por etapas começa a vazar a lógica de negócios. Em algum momento, você olha o que construiu e percebe que o formulário não é mais uma UI. É mais um processo de decisão, e a árvore de componentes é exatamente onde você os armazenou.

É aqui que eu acho que o modelo mental para formulários no React falha, e na verdade não é culpa de ninguém. A pilha RHF + Zod é excelente para o que foi projetada. A questão é que tendemos a continuar a usá-lo além do ponto em que suas abstrações correspondem ao problema porque a alternativa requer uma maneira totalmente diferente de pensar sobre as formas.

Este artigo é sobre essa alternativa. Para mostrar isso, construiremos exatamente o mesmo formulário de várias etapas duas vezes:

  1. Com React Hook Form + Zod conectado ao React Query para envio,
  2. Com SurveyJS, que trata um formulário como dados — um esquema JSON simples — em vez de uma árvore de componentes.

Os mesmos requisitos, a mesma lógica condicional, a mesma chamada de API no final. Em seguida, mapearemos exatamente o que mudou e o que permaneceu e apresentaremos uma maneira prática de decidir qual modelo você deve usar e quando.

O formulário que estamos construindo:

(Visualização grande)

Este formulário usará um fluxo de 4 etapas:

Etapa 1: detalhes

  • Nome (obrigatório),
  • E-mail (obrigatório, formato válido).

Etapa 2: pedido

  • Preço unitário,
  • Quantidade,
  • Taxa de imposto,
  • Derivado:

Etapa 3: conta e feedback

  • Você tem uma conta? (Sim/Não)
    • Se sim → nome de usuário + senha, ambos obrigatórios.
    • Se Não → e-mail já coletado na etapa 1.
  • Classificação de satisfação (1–5)
    • Se ≥ 4 → pergunte “Do que você gostou?”
    • Se ≤ 2 → pergunte “O que podemos melhorar?”

Etapa 4: revisão

  • Só aparece se total >= 100
  • Submissão final.

Isto não é extremo. Mas é o suficiente para expor diferenças arquitetônicas.

Parte 1: Orientado por Componente (Formulário React Hook + Zod)

Instalação

npm install react-hook-form zod @hookform/resolvers @tanstack/react-query

Esquema Zod

Vamos começar com o esquema Zod, porque geralmente é aí que a forma do formulário é estabelecida. Nas duas primeiras etapas – dados pessoais e entradas de pedidos – tudo é simples: strings obrigatórias, números com mínimos e uma enumeração. A parte interessante começa quando você tenta expressar as regras condicionais.

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),  
  quantity: z.number().min(1),  
  taxRate: z.number(),  
  hasAccount: z.enum(["Yes", "No"]),  
  username: z.string().optional(),  
  password: z.string().optional(),  
  satisfaction: z.number().min(1).max(5),  
  positiveFeedback: z.string().optional(),  
  improvementFeedback: z.string().optional(),  
}).superRefine((data, ctx) => {  
  if (data.hasAccount === "Yes") {  
    if (!data.username) {  
      ctx.addIssue({ code: "custom", path: ["username"], message: "Required" });  
    }  
    if (!data.password || data.password.length < 6) {  
      ctx.addIssue({ code: "custom", path: ["password"], message: "Min 6 characters" });  
    }  
  }

  if (data.satisfaction >= 4 && !data.positiveFeedback) {  
    ctx.addIssue({ code: "custom", path: ["positiveFeedback"], message: "Please share what you liked" });  
  }

  if (data.satisfaction <= 2 && !data.improvementFeedback) {  
    ctx.addIssue({ code: "custom", path: ["improvementFeedback"], message: "Please tell us what to improve" });  
  }  
});

export type FormData = z.infer;

Observe que username e password são digitados como optional() mesmo que sejam condicionalmente necessários porque o esquema de nível de tipo do Zod descreve o forma do objeto, não as regras que regem quando os campos são importantes.

O requisito condicional tem que viver dentro superRefineque é executado após a validação da forma e tem acesso ao objeto completo. Essa separação não é uma falha; é exatamente para isso que a ferramenta foi projetada: superRefine é para onde vai a lógica de campo cruzado quando não pode ser expressa na própria estrutura do esquema.

O que também é notável aqui é o que esse esquema não expressar. Não tem conceito de páginas, nenhum conceito de quais campos estão visíveis e em que ponto e nenhum conceito de navegação. Tudo isso viverá em outro lugar.

Componente de formulário

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; tax: number; total: number };

export function RHFMultiStepForm() {  
  const [step, setStep] = useState(0);

  const mutation = useMutation({
    mutationFn: async (payload: OrderPayload) => {
      const res = await fetch("/api/orders", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });
      if (!res.ok) throw new Error("Failed to submit");
      return res.json();
    },
  });

  const {  
    register,  
    control,  
    handleSubmit,  
    formState: { errors },  
  } = useForm({  
    resolver: zodResolver(formSchema),  
    defaultValues: {  
      price: 0,  
      quantity: 1,  
      taxRate: 0.1,  
      satisfaction: 3,  
      hasAccount: "No",  
    },  
  });  

  const price = useWatch({ control, name: "price" });  
  const quantity = useWatch({ control, name: "quantity" });  
  const taxRate = useWatch({ control, name: "taxRate" });  
  const hasAccount = useWatch({ control, name: "hasAccount" });  
  const satisfaction = useWatch({ control, name: "satisfaction" });  

  const subtotal = useMemo(() => (price ?? 0) * (quantity ?? 1), [price, quantity]);  
  const tax = useMemo(() => subtotal * (taxRate ?? 0), [subtotal, taxRate]);  
  const total = useMemo(() => subtotal + tax, [subtotal, tax]);  

  const onSubmit = (data: FormData) => mutation.mutate({ ...data, subtotal, tax, total });  

  const showSubmit = (step === 2 && total < 100) || (step === 3 && total >= 100)

  return (  
      
  );  
}

Veja a caneta [SurveyJS-03-RHF [forked]](https://codepen.io/smashingmag/pen/gbwwmNO) por sexta extinção.

Veja o Pen SurveyJS-03-RHF [forked] pela sexta extinção.

Há muita coisa acontecendo aqui e vale a pena desacelerar para perceber onde as coisas foram parar.

  • Os valores derivados – subtotal, tax, total — são calculados no componente via useWatch e useMemo porque eles dependem de valores de campos ativos e não há outro lugar natural para eles.
  • As regras de visibilidade para username, password, positiveFeedbacke improvementFeedback viva em JSX como condicionais embutidas.
  • A lógica de pular etapas – a página de revisão aparece apenas quando total >= 100 – está incorporado no showSubmit variável e a condição de renderização na etapa 3.
  • A navegação em si é apenas um useState contador que estamos incrementando manualmente.
  • React Query lida com novas tentativas, armazenamento em cache e invalidação. O formulário apenas chama mutation.mutate com dados validados.

Nada disso é errado, por si só. Este ainda é um React idiomático, e o componente tem bastante desempenho graças à forma como o RHF isola as re-renderizações.

Mas se você entregasse isso a alguém que não o escreveu e pedisse que explicasse sob quais condições a página de revisão apareceeles teriam que rastrear showSubmita condição de renderização da etapa 3 e a lógica do botão de navegação — três locais separados — para reconstruir uma regra que poderia ter sido declarada em uma linha.

O formulário funciona, sim, mas o comportamento não é realmente inspecionável como sistema. Tem que ser executado mentalmente.

Mais importante ainda, alterá-lo requer o envolvimento da engenharia. Mesmo um pequeno ajuste, como ajustar quando a etapa de revisão aparece, significa editar o componente, atualizar a validação, abrir uma solicitação pull, aguardar a revisão e implantar novamente.

Parte 2: Orientado por esquema (SurveyJS)

Agora vamos construir o mesmo fluxo usando um esquema.

Instalação

npm install survey-core survey-react-ui @tanstack/react-query
  • survey-core
    O mecanismo de tempo de execução independente de plataforma licenciado pelo MIT que alimenta a renderização de formulários do SurveyJS – a parte que nos interessa aqui. Ele pega um esquema JSON, constrói um modelo interno a partir dele e lida com tudo que de outra forma residiria em seu componente React: avaliando expressões de visibilidade, calculando valores derivados, gerenciando o estado da página, rastreando a validação e decidindo o que significa “completo” considerando quais páginas foram realmente mostradas.
  • survey-react-ui
    A UI/camada de renderização que conecta esse modelo ao React. É essencialmente um componente que é renderizado novamente sempre que o estado do mecanismo muda. As bibliotecas de UI SurveyJS também estão disponíveis para Angular, Vue3 e muitas outras estruturas.

Juntos, eles fornecem um tempo de execução de formulário de várias páginas totalmente funcional, sem escrever uma única linha de fluxo de controle.

O formato do esquema em si é, como dito antes, apenas um JSON – sem DSL ou qualquer coisa proprietária. Você pode incorporá-lo, importá-lo de um arquivo, buscá-lo em uma API ou armazená-lo em uma coluna de banco de dados e hidratá-lo em tempo de execução.

O mesmo formulário, como dados

Aqui está o mesmo formato, desta vez expresso como um objeto JSON. O esquema define tudo: estrutura, validação, regras de visibilidade, cálculos derivados, navegação na página – e entrega tudo a um Model que o avalia em tempo de execução. Aqui está o que parece na íntegra:

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: "email", text: "Invalid email" }] }  
      ]  
    },  
    {  
      name: "order",  
      elements: [  
        { type: "text", name: "price", inputType: "number", defaultValue: 0 },  
        { type: "text", name: "quantity", inputType: "number", defaultValue: 1 },  
        {  
          type: "dropdown",  
          name: "taxRate",  
          defaultValue: 0.1,  
          choices: [  
            { value: 0.05, text: "5%" },  
            { value: 0.1, text: "10%" },  
            { value: 0.15, text: "15%" }  
          ]  
        },  
        {  
          type: "expression",  
          name: "subtotal",  
          expression: "{price} * {quantity}"  
        },  
        {  
          type: "expression",  
          name: "tax",  
          expression: "{subtotal} * {taxRate}"  
        },  
        {  
          type: "expression",  
          name: "total",  
          expression: "{subtotal} + {tax}"  
        }  
      ]  
    },  
    {  
      name: "account",  
      elements: [  
        {  
          type: "radiogroup",  
          name: "hasAccount",  
          choices: ["Yes", "No"]  
        },  
        {  
          type: "text",  
          name: "username",  
          visibleIf: "{hasAccount} = 'Yes'",  
          isRequired: true  
        },  
        {  
          type: "text",  
          name: "password",  
          inputType: "password",  
          visibleIf: "{hasAccount} = 'Yes'",  
          isRequired: true,  
          validators: [{ type: "text", minLength: 6, text: "Min 6 characters" }]  
        },  
        {  
          type: "rating",  
          name: "satisfaction",  
          rateMin: 1,  
          rateMax: 5  
        },  
        {  
          type: "comment",  
          name: "positiveFeedback",  
          visibleIf: "{satisfaction} >= 4"  
        },  
        {  
          type: "comment",  
          name: "improvementFeedback",  
          visibleIf: "{satisfaction} <= 2"  
        }  
      ]  
    },  
    {  
      name: "review",  
      visibleIf: "{total} >= 100",  
      elements: []  
    }  
  ]  
};

Compare isso com a versão RHF por um momento.

  • O superRefine bloquear aquele condicionalmente necessário username e password desapareceu. visibleIf: "{hasAccount} = 'Yes'" combinado com isRequired: true lida com ambas as preocupações juntas, no próprio campo, onde você esperaria encontrá-las.
  • O useWatch + useMemo cadeia que calculou subtotal, taxe total é substituído por três expression campos que fazem referência entre si por nome.
  • A condição da página de revisão, que na versão RHF só era reconstruída rastreando showSubmito branch de renderização da etapa 3.
  • E finalmente, a lógica do botão de navegação é uma única visibleIf propriedade no objeto de página.

A mesma lógica está aí. Acontece que o esquema oferece a ele um local para morar, onde é visível isoladamente, em vez de se espalhar pelo componente.

Além disso, observe que o esquema usa type: 'expression' para subtotal, imposto e total. A expressão é somente leitura e usada principalmente para exibir valores calculados. SurveyJS também suporta type: 'html' para conteúdo estático, mas para valores calculados, expression é a escolha certa.

Agora, para o lado React.

Renderização e envio

Muito simples. Arame onComplete à sua API da mesma maneira – via useMutation ou simples 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 = await fetch("/api/orders", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
      if (!res.ok) throw new Error("Failed to submit");
      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 avoids re-registering handler every render (mutation object identity changes)

  return (
    <>
        
      {mutation.isError && 

Error: {mutation.error.message}

} > ); }

Veja a caneta [SurveyJS-03-SurveyJS [forked]](https://codepen.io/smashingmag/pen/emddWNV) por sexta extinção.

Veja o Pen SurveyJS-03-SurveyJS [forked] pela sexta extinção.
  • onComplete dispara quando o usuário chega ao final do último visível página. Então se total nunca ultrapassa 100 e a página de revisão é ignorada, ela ainda é acionada corretamente porque o SurveyJS avalia a visibilidade antes de decidir o que significa “última página”.
  • Então, sender.data contém todas as respostas junto com os valores calculados (subtotal, tax, total) como campos de primeira classe, portanto a carga útil da API é idêntica à versão RHF montada manualmente em onSubmit.
  • O mutationRef padrão é o mesmo que você usaria em qualquer lugar onde precisasse de um manipulador de eventos estável sobre um valor que muda a cada renderização – nada específico do SurveyJS sobre isso.

O componente React não contém mais nenhuma lógica de negócios. Não há useWatchsem JSX condicional, sem contador de passos, sem useMemo cadeia, não superRefine. O React está fazendo o que realmente é bom: renderizar um componente e conectá-lo a uma chamada de API.

O que saiu do React?

PreocupaçãoPilha RHFPesquisa JS
VisibilidadeRamos JSXvisibleIf
Valores derivadosuseWatch / useMemoexpression
Regras entre campossuperRefineCondições do esquema
Navegaçãostep estadoPágina visibleIf
Localização da regraDistribuído entre arquivosCentralizado no esquema

O que permanece no React é o layout, o estilo, a fiação de envio e a integração do aplicativo, ou seja, as coisas para as quais o React foi realmente projetado.

Todo o resto foi movido para o esquema e, como o esquema é apenas um objeto JSON, ele pode ser armazenado em um banco de dados, versionado independentemente do código do seu aplicativo ou editado por meio de ferramentas internas sem a necessidade de implantação.

Um gerente de produto que precisa alterar o limite que aciona a página de revisão pode fazer isso sem tocar no componente. Essa é uma diferença operacional significativa para equipes onde o comportamento do formulário evolui com frequência e nem sempre é orientado por engenheiros.

Quando usar cada abordagem?

Aqui está uma boa regra que funciona para mim: imagine excluir o formulário completamente. O que você perderia?

  • Se forem telas, você deseja formulários baseados em componentes.
  • Se for lógica de negócios, como limites, regras de ramificação e requisitos condicionais que codificam decisões reais, você deseja um mecanismo de esquema.

Da mesma forma, se as mudanças que ocorrerem forem principalmente sobre rótulos, campos e layout, o RHF será útil para você. Se forem sobre condições, resultados e regras que sua equipe de operações ou jurídica pode precisar ajustar em uma tarde de terça-feira sem registrar um ticket, o modelo de esquema com SurveyJS é a opção mais honesta.

Estas duas abordagens não estão realmente em concorrência entre si. Eles abordam diferentes classes de problemas, e o erro que vale a pena evitar é a incompatibilidade entre a abstração e o peso da lógica — tratando um sistema de regras como um componente porque essa é a ferramenta familiar, ou recorrendo a um mecanismo de política porque um formulário cresceu para três etapas e adquiriu um campo condicional.

A forma que construímos aqui fica deliberadamente perto do limite, complexa o suficiente para expor a diferença, mas não tão extrema que a comparação pareça fraudulenta. A maioria dos formulários reais que se tornaram difíceis de manejar em sua base de código provavelmente ficam próximos ao mesmo limite, e a questão geralmente é se alguém nomeou o que eles realmente são.

Use React Hook Form + Zod quando:

  • Os formulários são orientados para CRUD;
  • A lógica é superficial e orientada pela UI;
  • Os engenheiros são donos de todo comportamento;
  • O back-end continua sendo a fonte da verdade.

Use SurveyJS quando:

  • Os formulários codificam decisões de negócios;
  • As regras evoluem independentemente da IU;
  • A lógica deve ser visível, auditável ou versionada;
  • Os não-engenheiros influenciam o comportamento;
  • O mesmo formulário deve ser executado em vários frontends.
(sim)

Fonte: Tecmundo, Olhar Digital, MeioBit

Mais recentes

PUBLICIDADE

WP Twitter Auto Publish Powered By : XYZScripts.com