Spike: React Hook Form vs. Formik para gerenciamento de formulários
O projeto @zeloclub/clinic (v0.0.1) roda sobre React 19.2.4 + Next.js 16.2.6, com TypeScript 5, Tailwind 4 e um design system próprio baseado em Radix UI (@radix-ui/*) e pacotes internos (@zeloclub/ui, @zeloclub/domain, @zeloclub/types). O manifesto já lista react-hook-form@^7, zod@^3 e @hookform/resolvers@^3 como dependências de formulários, então este spike serve tanto para validar formalmente a escolha existente quanto para responder objetivamente à recorrente pergunta "e o Formik?". A stack usa componentes controlados de UI (Radix Select, Checkbox, RadioGroup, Switch), o que torna a estratégia de controle de estado do formulário um critério central.
Objetivo e critérios de decisão
- Compatibilidade com React 19 / Next.js 16 (App Router, Server Components) — precisa funcionar em Client Components sem workarounds.
- Custo de bundle — impacto em gzip no client-side (crítico para páginas do App Router).
- Modelo de re-render — quantidade de re-renders por keystroke em formulários grandes (fichas clínicas tendem a ter muitos campos).
- Integração com Zod — o projeto já padroniza validação com
zod, então o resolver precisa ser first-class. - Integração com componentes controlados do Radix/Base UI — ergonomia para
Controller/adapters. - DX + TypeScript — inferência de tipos a partir do schema, autocomplete de nomes de campos.
- Manutenção e adoção — frequência de releases, downloads, saúde do projeto.
Opções analisadas
Opção 1 — React Hook Form (react-hook-form + @hookform/resolvers)
Biblioteca baseada em componentes não-controlados por padrão (refs), o que reduz re-renders. A validação é plugada via @hookform/resolvers, que possui adapter oficial para Zod.
Dados (npm/bundlephobia, 2026-07-16): versão 7.81.0, licença MIT, ~52,97 milhões de downloads/semana, bundle 12.6 kB gzip. O @hookform/resolvers (5.4.0, MIT) adiciona apenas 0.5 kB gzip e tem ~37 milhões de downloads/semana.
Prós
- Modelo por refs: campos nativos (
register) praticamente não causam re-render do formulário inteiro — vantagem real em fichas longas. - Integração Zod madura e tipada:
useForm<z.infer<typeof schema>>dá autocomplete denamee tipos de erro. Controlleré a ponte natural para componentes controlados do Radix/Base UI já usados no projeto.- Ecossistema dominante; já é a dependência atual, custo de adoção zero.
- Compatível com React 19 e Next.js 16 sem ajustes.
Contras
- API baseada em refs pode confundir quem vem de formulários controlados;
Controlleré obrigatório para todo componente Radix. - Debug de campos "esquecidos" (não registrados) é menos óbvio.
- Atenção de versão: RHF 7.81 pareia com
@hookform/resolversv5; o manifesto ainda fixa@hookform/resolvers@^3. É preciso subir o resolver.
// components/forms/patient-form.tsx
"use client";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import * as RadioGroup from "@radix-ui/react-radio-group";
const patientSchema = z.object({
fullName: z.string().min(3, "Nome muito curto"),
bloodType: z.enum(["A", "B", "AB", "O"]),
});
type PatientForm = z.infer<typeof patientSchema>;
export function PatientForm() {
const {
register,
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm<PatientForm>({
resolver: zodResolver(patientSchema),
defaultValues: { fullName: "", bloodType: "O" },
});
const onSubmit = async () => {
.(data);
};
(
);
}
Opção 2 — Formik (formik + validação Zod via toFormikValidationSchema)
Biblioteca baseada em estado controlado: todo o valor do formulário vive no state do React e é atualizado a cada onChange. Historicamente pareada com Yup, mas integra com Zod por meio de utilitários da comunidade (zod-formik-adapter).
Dados (npm/bundlephobia, 2026-07-16): versão 2.4.9, licença Apache-2.0, ~4,13 milhões de downloads/semana, bundle 12.8 kB gzip. Se optar por Yup em vez de Zod, adiciona-se yup 1.7.1 (MIT, ~13.1 kB gzip) — o que duplicaria a camada de validação, já que o projeto padroniza Zod.
Prós
- API de estado controlado é intuitiva para quem já pensa em "estado do React".
<Formik>/useFormikcobrem casos comuns com pouca cerimônia.- Boa documentação legada e muitos exemplos.
Contras
- Re-render a cada keystroke do formulário inteiro por padrão — problema em fichas clínicas grandes com muitos campos controlados.
- Cadência de manutenção lenta (última versão estável 2.4.x há bastante tempo); ritmo de evolução bem abaixo do RHF.
- Integração com Zod não é oficial — depende de adapter de terceiro, gerando risco de compatibilidade a longo prazo. O caminho "oficial" é Yup, o que conflita com o padrão Zod do projeto.
- Ergonomia com componentes controlados do Radix exige boilerplate manual (
setFieldValue/getFieldProps). - ~13x menos downloads/semana que RHF; tendência de mercado desfavorável.
// components/forms/patient-form-formik.tsx
"use client";
import { useFormik } from "formik";
import { toFormikValidationSchema } from "zod-formik-adapter";
import { z } from "zod";
import * as RadioGroup from "@radix-ui/react-radio-group";
const patientSchema = z.object({
fullName: z.string().min(3, "Nome muito curto"),
bloodType: z.enum(["A", "B", "AB", "O"]),
});
type PatientForm = z.infer<typeof patientSchema>;
export function PatientFormFormik() {
const formik = useFormik<PatientForm>({
initialValues: { fullName: "", bloodType: "O" },
validationSchema: toFormikValidationSchema(patientSchema),
onSubmit: async (values) => {
await Promise.resolve(values);
},
});
(
);
}
Comparação
Recomendação
Adotar formalmente o React Hook Form como biblioteca padrão de formulários do @zeloclub/clinic, mantendo Zod como camada única de validação via @hookform/resolvers/zod. A escolha não é apenas confirmar o que já está no manifesto: ela se sustenta nos critérios técnicos deste spike. O modelo por refs do RHF reduz drasticamente re-renders em formulários grandes — cenário típico de fichas clínicas e cadastros extensos —, enquanto o Formik re-renderiza o formulário inteiro a cada tecla, o que degrada a experiência exatamente nas telas mais pesadas do domínio clínico.
O segundo fator decisivo é a coerência com o padrão Zod já estabelecido na stack. O RHF trata Zod como cidadão de primeira classe através do resolver oficial (0.5 kB gzip), com inferência de tipos que dá autocomplete de nomes de campos e tipagem de erros de graça. No Formik, o caminho idiomático é Yup — o que introduziria uma segunda biblioteca de validação (~13.1 kB gzip) ou, alternativamente, um adapter Zod de terceiro sem garantia de manutenção. Somado à cadência de releases muito mais ativa do RHF e à sua adoção massivamente maior (~52,97 mi vs. ~4,13 mi downloads/semana, npm, 2026-07-16), o risco de estagnação pende claramente para o lado do Formik.
Quando reconsiderar: se o time optasse por padronizar validação em Yup por outro motivo estratégico, ou se a maior parte dos formulários fosse trivial (1–3 campos) e a equipe valorizasse mais a familiaridade com estado controlado do que performance — nenhum dos dois é o caso aqui. O único ajuste necessário é técnico: subir @hookform/resolvers de ^3 para a linha 5.x, exigida pelo RHF 7.81.
Plano de prova de conceito
O objetivo é validar RHF + Zod contra os componentes Radix reais do projeto e medir re-renders. Timebox total: ~6 horas.
- (0,5h) Alinhar versões. Atualizar o resolver para a linha compatível com RHF 7.81:
npm install react-hook-form@^7 @hookform/resolvers@^5 zod@^3 - (1h) Formulário representativo. Recriar um formulário real do domínio (ex.: cadastro de paciente) com pelo menos 8 campos, incluindo
@radix-ui/react-select,react-checkbox,react-radio-groupereact-switchviaController. - (1h) Schema Zod + inferência. Definir o schema em
zod, aplicarzodResolvere confirmar queuseForm<z.infer<typeof schema>>fornece autocomplete denamee tipagem de erros; rodarnpm run type-check. - (1h) Medir re-renders. Instrumentar com React DevTools Profiler (ou
why-did-you-renderem dev) e comparar re-renders por keystroke em relação a uma versão controlada equivalente. - (1h) Integração com estado global. Validar submit persistindo em
zustande consumo via@zeloclub/api-client, além de mensagens i18n vianext-intl. - (0,5h) Server Components. Confirmar uso com
"use client"em página do App Router do Next 16, garantindo que o formulário fica isolado no client boundary. - (1h) Documentar. Registrar padrão de
Controllerpara cada componente Radix no design system e abrir helper reutilizável em@zeloclub/uise fizer sentido.
Riscos e mitigação
Critérios de aceite do spike
- RHF 7.x +
@hookform/resolvers@^5+zod@^3instalados enpm run type-checkpassando. - Formulário-piloto com ≥8 campos, incluindo ≥3 componentes controlados do Radix via
Controller. - Inferência de tipos confirmada (
z.infer+ autocomplete denamee erros). - Medição de re-renders documentada, evidenciando vantagem do modelo não-controlado.
- Submit integrado a
zustand+@zeloclub/api-clientfuncionando. - Formulário validado dentro de página do App Router (Next 16) com client boundary correto.
- Decisão registrada e padrão de wrappers Radix+RHF proposto para
@zeloclub/ui.