Dez tickets de bugs do mundo real sobre comportamento de formulários, controles de input, validação e submissão. Cada entrada mostra o ticket não técnico, o diagnóstico, uma correção concisa e por que a correção está correta.
Bugs de formulário se concentram em quatro causas: onChange ausente (controlado), assinatura de handler incorreta, tipo de input incorreto e valores padrão ausentes.
Sempre verifique se o input é controlado, não controlado ou um híbrido - a maioria dos bugs de formulário se esconde nessa fronteira.
Por que isso funciona: React exige value e onChange para inputs controlados. O onChange é o que realmente muta o estado a cada pressionamento de tecla, o que então re-renderiza o input com o novo value.
Por que isso funciona:e.preventDefault() cancela o submit nativo, então o navegador não recarrega. O handler executa sua lógica de salvamento com o estado atual do formulário em vez de descartar os dados.
"Edito um nome no meio, digito um caractere, e o cursor pula para o final. Está enlouquecendo a equipe."
Diagnóstico: O input está sendo desmontado e remontado a cada pressionamento de tecla - geralmente porque está definido dentro de um componente pai que re-renderiza, ou por causa de uma key que muda.
Correção:
"use client";// Defina componentes estáveis FORA de pais que re-renderizamfunction NameInput({ value, onChange }: { value: string; onChange: (v: string) => void }) { return <input value={value} onChange={(e) => onChange(e.target.value)} />;}export function Page() { const [name, setName] = useState(""); return <NameInput value={name} onChange={setName} />;}
Por que isso funciona: Componentes definidos dentro de outro componente são recriados a cada renderização, então React os trata como novos tipos e desmonta/remonta o input - perdendo o estado de seleção. Definir o componente no escopo do módulo mantém a instância do input estável entre as renderizações.
"Grande formulário de checkout. Se o e-mail for inválido, mostramos o erro, mas todos os outros campos esvaziam ao mesmo tempo."
Diagnóstico: O formulário usa defaultValue em inputs não controlados sem os padrões useForm, então re-renderiza com novas chaves ou remonta na validação, limpando o estado do DOM.
Por que isso funciona: React Hook Form gerencia o estado do campo em um store baseado em ref, então erros de validação disparam re-renderizações sem remontar inputs ou perder seus valores. Os valores dos campos sobrevivem a cada ciclo de validação.
"'Lembrar-me' do formulário de login está travado. O estado mostra como desmarcado visualmente, mas a API sempre recebe true."
Diagnóstico: A caixa de seleção está vinculada com value em vez de checked, então React renderiza o atributo value como uma string, mas nunca controla realmente o estado de marcado.
Por que isso funciona: Caixas de seleção usam checked e e.target.checked - não value e e.target.value. O atributo value em uma caixa de seleção é a string literal enviada na submissão quando marcada, não se ela está selecionada.
Por que isso funciona:valueAsNumber retorna um número ou NaN diretamente. Armazenar null quando o campo está vazio evita a coerção de strings vazias para 0 e corresponde a como uma API JSON espera um valor ausente.
"Escolhemos um aniversário no seletor de data, salvamos, e a página de detalhes do cliente mostra o dia anterior. Reproduzível para todos."
Diagnóstico:new Date("1990-05-14") é analisado como meia-noite UTC, mas toLocaleDateString() renderiza em hora local - que é o dia anterior em qualquer fuso horário a oeste do UTC.
Correção:
export function formatBirthday(iso: string): string { const [y, m, d] = iso.split("-").map(Number); return new Date(y, m - 1, d).toLocaleDateString();}
Por que isso funciona: Construir a Data com argumentos numéricos a cria em hora local, correspondendo à data que o usuário realmente escolheu. Strings ISO implicam UTC e silenciosamente mudam entre limites de fuso horário - o caso de data de calendário precisa de um parser de data de calendário.
Por que isso funciona: Pressionar Enter dentro de um formulário já o submete - não há necessidade de escutar Enter separadamente. Remover o handler onKeyDown e confiar em onSubmit torna o único caminho de submit o único caminho.
"Desabilitamos certos campos quando eles são 'travados'. Ao salvar, esses campos estão faltando no corpo da requisição, mesmo que estejam visíveis."
Diagnóstico:FormData nativo pula inputs desabilitados por design - eles não são considerados parte da submissão do formulário.
Correção:
// Ou:<input name="planId" value={planId} readOnly />// Ou inclua o valor explicitamente ao construir o payload:const payload = { ...Object.fromEntries(new FormData(form)), planId };
Por que isso funciona:readOnly mantém o campo não editável, mas ainda submetível, enquanto disabled o remove completamente do FormData. Se a UX precisar de disabled, o handler de submissão deve mesclar os valores ausentes manualmente.
"Clientes podem anexar arquivos, mas a barra de progresso está quebrada. Ela fica em 0%, depois pula para 100% quando o upload termina."
Diagnóstico: O upload usa fetch, que não expõe eventos de progresso do lado da requisição. Apenas XMLHttpRequest (ou uma abstração ciente de streaming) relata bytes de upload.
Por que isso funciona:xhr.upload.progress é a única API do navegador que relata bytes enviados do lado da requisição. Os corpos de requisição ReadableStream do Fetch não expõem progresso de forma cross-browser hoje, então XHR ainda é a ferramenta certa para UIs de progresso de upload.