Pular para o conteúdo

ADR-0013 Formulários

Formulários padrão da empresa — TanStack Form com schemas Zod partilhados, componentes Field do shadcn/ui e server functions POST; JavaScript obrigatório por predefinição.

Decisão
TanStack Form + Zod + shadcn/ui FieldConstruir a app
Estado
Aceite
Data
Âmbito
Predefinição da empresa para todas as novas apps

Todos os produtos têm formulários: início de sessão, definições, ecrãs de criação/edição, fluxos com vários passos. As decisões à volta estão tomadas:

  • Validação: Zod 4, um schema por forma, partilhado entre cliente e servidor (ADR-0005) — que já assume o TanStack Form
  • Componentes: shadcn/ui sobre Base UI (ADR-0011); os componentes Field do shadcn/ui (Field, FieldLabel, FieldDescription, FieldError) trazem guias para o TanStack Form e o React Hook Form
  • Escritas: server functions POST chamadas a partir de useMutation, seguidas de invalidação (ADR-0012); toda a escrita passa pelo limitador (ADR-0010)
  • Os formulários de autenticação chamam o SDK cliente do Better Auth (ADR-0009), que precisa de JavaScript

O que está em aberto: a biblioteca de estado de formulários, como os erros chegam ao utilizador e se os formulários têm de funcionar antes de o JavaScript carregar.

Dois factos do Start moldam a resposta:

  • As server functions são endpoints. Qualquer pessoa as pode chamar diretamente, não só o nosso formulário — por isso o servidor valida e verifica a autenticação em cada chamada; a validação no cliente é UX, nunca proteção
  • A proteção CSRF para server functions vem de createCsrfMiddleware(), que verifica Sec-Fetch-Site, Origin e Referer. O Start instala-o automaticamente apenas quando a app não tem src/start.ts — e todas as apps têm um, por causa do limitador (ADR-0010)

Prós

  • Mesma equipa do Router, do Query e do Start; MIT; 1.x estável (1.33.5)
  • Adaptador oficial para o Start (@tanstack/react-form-start) para validação no servidor e submissões sem JS quando precisarmos
  • Os schemas Zod 4 encaixam diretamente através do Standard Schema — sem pacote de resolver
  • createFormHook liga os nossos próprios componentes de campo uma só vez; os formulários passam então a usar form.AppField com nomes de campo tipados — uma gralha é um erro de compilação
  • Subscrições de granularidade fina: uma tecla premida volta a renderizar apenas o campo, não o formulário

Contras

  • Ecossistema mais pequeno e menos exemplos do que o React Hook Form
  • A API de render props (children={(field) => …}) é mais verbosa sem createFormHook
  • A v2 está em alpha; vem aí uma atualização major

Prós

  • O maior ecossistema de formulários e mais exemplos; o shadcn/ui tem um guia para ele; 7.x estável (7.90.0)
  • Zod via @hookform/resolvers (6.x, suporta Zod 4)

Contras

  • Sem integração com o Start; a validação no servidor e a submissão sem JS ficam por nossa conta
  • A v8 está em beta — uma atualização major vem aí em breve
  • Um segundo pacote adaptador de validação; o pressuposto do ADR-0005 de que usamos TanStack Form deixaria de valer

Prós: construído em torno do progressive enhancement e do FormData; suporte para Zod 4. Contras: pensado para server actions do Remix e do Next.js; sem adaptador para o TanStack Start e sem guia do shadcn/ui.

4. Sem biblioteca (formulários nativos + useActionState do React 19)

Seção intitulada “4. Sem biblioteca (formulários nativos + useActionState do React 19)”

Prós: zero dependências. Contras: erros por campo, estado touched/dirty, arrays e verificações assíncronas reconstruídos em cada produto.

  • JS obrigatório: os formulários submetem através de useMutation; ficam desativados até à hidratação. Um só caminho de código, alinhado com o ADR-0012 e com o SDK cliente do Better Auth
  • Progressivo por predefinição: todos os formulários fazem POST de FormData para serverFn.url e funcionam sem JavaScript. Robusto em redes lentas, mas todos os formulários precisam de parsing de FormData, erros renderizados no servidor e redirecionamentos — e os formulários de autenticação não encaixam no SDK cliente

TanStack Form com schemas Zod partilhados e componentes Field do shadcn/ui é o padrão da empresa para formulários. Os formulários exigem JavaScript por predefinição; o progressive enhancement é opt-in por formulário.

Em concreto:

  • Pacote: @tanstack/react-form 1.x. Acrescentar @tanstack/react-form-start (mesma versão) apenas para formulários sem JS
  • Um form hook por app: createFormHookContexts() + createFormHook() em src/components/form/, ligando componentes de campo construídos sobre o Field do shadcn/ui (TextField, TextareaField, SelectField, CheckboxField, SwitchField, …) e componentes de formulário (SubmitButton, FormError). Os formulários usam useAppForm e form.AppField; os sub-formulários usam withForm. Os produtos estendem este conjunto — não ligam form.Field à mão em cada ecrã
  • Erros acessíveis, seguindo o guia do shadcn/ui: data-invalid no Field, aria-invalid no controlo, FieldError renderizado quando isTouched && !isValid. Os componentes de campo tratam disto, pelo que os ecrãs não se podem esquecer
  • Um schema Zod por formulário, num módulo que ambos os lados importam (sem imports exclusivos do servidor). O cliente usa-o como validador do formulário (validators: { onSubmit: schema }; acrescentar onBlur em formulários longos). A server function valida com o mesmo schema: createServerFn({ method: "POST" }).validator(schema). O servidor valida sempre; a validação no cliente serve apenas para dar feedback mais rápido
  • Submeter através de uma mutação: onSubmit: ({ value }) => mutation.mutateAsync(value), em que a mutação chama a server function POST via useServerFn e invalida em caso de sucesso (ADR-0012). O botão de submissão mostra isSubmitting e impede submissões duplicadas
  • Os erros do servidor são mapeados de volta para os campos. As falhas de regras de negócio (email já registado, stock esgotado) devolvem erros indexados por campo a partir da server function e são renderizadas pelo mesmo FieldError; os validadores ao nível do formulário do TanStack Form podem definir erros de campo ({ fields: { email: "…" } }). Os erros inesperados mostram uma única mensagem ao nível do formulário — nunca um stack trace nem um erro em bruto da base de dados
  • Toda a server function de escrita verifica ela própria a sessão (middleware de autenticação ou verificação no handler) — o beforeLoad de uma rota protege a página, não o endpoint
  • CSRF: src/start.ts regista createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" }) ao lado do limitador, porque definir src/start.ts desativa o middleware automático do Start. Passar sempre origin: APP_ORIGIN — atrás do reverse proxy, o URL do pedido não é uma fonte fiável para a origem pública (ADR-0017)
  • Os formulários com JS obrigatório são seguros antes da hidratação: <form method="post"> para que uma submissão nativa nunca possa pôr valores (palavras-passe) no URL, e o botão de submissão fica desativado até hidratar (useHydrated() de @tanstack/react-router)
  • Os formulários de autenticação usam o mesmo form hook e os mesmos componentes de campo; o onSubmit chama o cliente do Better Auth (por exemplo authClient.signIn.email) em vez de uma server function e depois limpa as caches como no ADR-0012
  • Os formulários sem JS são opt-in — para formulários públicos que têm de funcionar sem JavaScript (por exemplo, um formulário de contacto): formOptions e createServerValidate do @tanstack/react-form-start, <form action={serverFn.url} method="post" encType="multipart/form-data">, e getFormData + mergeForm para mostrar os erros do servidor após o ciclo de ida e volta

Os fatores decisivos: a mesma equipa e o mesmo ciclo de lançamentos do Router, do Query e do Start; schemas Zod sem adaptador; componentes de campo tipados e pré-ligados que tornam os erros acessíveis a predefinição; e um único caminho de submissão através de server functions e mutações. Aceitamos o ecossistema mais pequeno e a próxima atualização para a v2.

Mais fácil:

  • Um só schema valida o formulário, tipa os seus valores e protege a server function — cliente e servidor não conseguem divergir
  • Os ecrãs compõem componentes de campo já prontos; rótulos, descrições e estados de erro acessíveis vêm sem esforço
  • Toda a escrita segue o mesmo caminho: formulário → mutação → server function POST → limitador → verificação CSRF → validação → handler → invalidação

Mais difícil:

  • O conjunto de componentes de campo em src/components/form/ é nosso para construir e manter; é o primeiro candidato para o registry partilhado do shadcn/ui (ADR-0011)
  • A maioria dos exemplos da comunidade usa React Hook Form; adaptá-los em vez de os copiar
  • O src/start.ts tem de manter o middleware CSRF — removê-lo, ou restringir o seu filtro, abre em silêncio todas as server functions a pedidos cross-site. Acrescentar um teste que garanta que um POST cross-site é rejeitado
  • Os formulários não funcionam antes da hidratação em ligações lentas; o botão de submissão desativado torna isso visível em vez de silencioso
  • O TanStack Form v2 está em alpha; planear uma janela de atualização quando sair

Do que abdicamos: do ecossistema do React Hook Form e do progressive enhancement como predefinição. Aceite — um só ciclo de lançamentos e um só caminho de submissão pesam mais numa predefinição da empresa; os produtos com requisitos de funcionamento sem JS fazem opt-in por formulário.