Eventos de Formulário
Lide com envios de formulário, alterações de entrada e redefinições de formulário em componentes React.
Busque em todas as páginas da documentação
Lide com envios de formulário, alterações de entrada e redefinições de formulário em componentes React.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| Prop do React | Tipo TypeScript | Dispara Quando | Notas |
|---|---|---|---|
onChange | React.ChangeEvent<T> | Valor de um input, select ou textarea muda | No React, dispara a cada pressionamento de tecla (não apenas blur) |
onInput | React.FormEvent<T> | Valor de um input muda | Quase idêntico a onChange no React -- prefira onChange |
onSubmit | React.FormEvent<HTMLFormElement> | Formulário é enviado (tecla Enter ou botão submit) | Chame e.preventDefault() para tratamento no lado do cliente |
onReset | React.FormEvent<HTMLFormElement> | Botão de reset do formulário é clicado | Raramente usado -- a maioria dos apps gerencia o reset via estado |
onInvalid | React.FormEvent<T> | Validação integrada falha em um input | Dispara antes do navegador mostrar sua tooltip de validação |
Cartão de receita de referência rápida -- pronto para copiar e colar.
// Input controlado com onChange
function ControlledInput() {
const [name, setName] = React.useState("");
return (
<input
value={name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)}
placeholder="Digite o nome"
/>
);
}
// Envio de formulário com preventDefault
function SimpleForm() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
console.log("Email:", formData.get("email"));
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<button type="submit">Enviar</button>
</form>
);
}
// Extração de FormData sem estado controlado
function UncontrolledForm() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
console.log(data); // { email: "...", password: "..." }
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
<input name="password" type="password" />
<button type="submit">Entrar</button>
</form>
);
}Quando usar isso: Você precisa lidar com a entrada do usuário em formulários -- campos de texto, selects, checkboxes, uploads de arquivos ou envios de formulário com validação.
"use client";
import { useState, useCallback } from "react";
type FormData = {
username: string;
email: string;
password: string;
role: string;
agree: boolean;
};
type FormErrors = Partial<Record<keyof FormData, string>>;
const INITIAL_STATE: FormData = {
username: "",
email: "",
password: "",
role: "",
agree: false,
};
function validate(data: FormData): FormErrors {
const errors: FormErrors = {};
if (data.username.length < 3) errors.username = "Pelo menos 3 caracteres";
if (!data.email.includes("@")) errors.email = "Endereço de e-mail inválido";
if (data.password.length < 8) errors.password = "Pelo menos 8 caracteres";
if (!data.role) errors.role = "Selecione um cargo";
if (!data.agree) errors.agree = "Você deve concordar com os termos";
return errors;
}
export default function SignupForm() {
const [formData, setFormData] = useState<FormData>(INITIAL_STATE);
const [errors, setErrors] = useState<FormErrors>({});
const [submitted, setSubmitted] = useState(false);
const handleChange = useCallback(
(
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, type } = e.target;
const value =
type === "checkbox"
? (e.target as HTMLInputElement).checked
: e.target.value;
setFormData((prev) => ({ ...prev, [name]: value }));
// Limpa o erro para este campo ao mudar
setErrors((prev) => ({ ...prev, [name]: undefined }));
},
[]
);
const handleSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const validationErrors = validate(formData);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
console.log("Dados de cadastro:", formData);
setSubmitted(true);
},
[formData]
);
const handleReset = useCallback(() => {
setFormData(INITIAL_STATE);
setErrors({});
setSubmitted(false);
}, []);
const handleInvalid = useCallback(
(e: React.FormEvent<HTMLInputElement>) => {
e.preventDefault(); // Impede a tooltip do navegador
const name = e.currentTarget.name as keyof FormData;
setErrors((prev) => ({
...prev,
[name]: e.currentTarget.validationMessage,
}));
},
[]
);
if (submitted) {
return (
<div style={{ padding: 24 }}>
<p>Conta criada para {formData.username}.</p>
<button onClick={handleReset}>Cadastrar outro</button>
</div>
);
}
return (
<form
onSubmit={handleSubmit}
onReset={handleReset}
noValidate
style={{
maxWidth: 400,
display: "flex",
flexDirection: "column",
gap: 16,
padding: 24,
}}
>
<h2 style={{ margin: 0 }}>Cadastre-se</h2>
<div>
<label htmlFor="username">Nome de usuário</label>
<input
id="username"
name="username"
value={formData.username}
onChange={handleChange}
onInvalid={handleInvalid}
required
minLength={3}
style={{ display: "block", width: "100%", padding: "8px" }}
/>
{errors.username && (
<span style={{ color: "#dc2626", fontSize: 14 }}>
{errors.username}
</span>
)}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
onInvalid={handleInvalid}
required
style={{ display: "block", width: "100%", padding: "8px" }}
/>
{errors.email && (
<span style={{ color: "#dc2626", fontSize: 14 }}>
{errors.email}
</span>
)}
</div>
<div>
<label htmlFor="password">Senha</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
onInvalid={handleInvalid}
required
minLength={8}
style={{ display: "block", width: "100%", padding: "8px" }}
/>
{errors.password && (
<span style={{ color: "#dc2626", fontSize: 14 }}>
{errors.password}
</span>
)}
</div>
<div>
<label htmlFor="role">Cargo</label>
<select
id="role"
name="role"
value={formData.role}
onChange={handleChange}
style={{ display: "block", width: "100%", padding: "8px" }}
>
<option value="">Selecione um cargo...</option>
<option value="developer">Desenvolvedor</option>
<option value="designer">Designer</option>
<option value="manager">Gerente</option>
</select>
{errors.role && (
<span style={{ color: "#dc2626", fontSize: 14 }}>
{errors.role}
</span>
)}
</div>
<div>
<label>
<input
name="agree"
type="checkbox"
checked={formData.agree}
onChange={handleChange}
/>{" "}
Eu concordo com os termos
</label>
{errors.agree && (
<span style={{ color: "#dc2626", fontSize: 14, display: "block" }}>
{errors.agree}
</span>
)}
</div>
<div style={{ display: "flex", gap: 8 }}>
<button type="submit" style={{ padding: "8px 16px" }}>
Criar Conta
</button>
<button type="reset" style={{ padding: "8px 16px" }}>
Limpar
</button>
</div>
</form>
);
}O que isso demonstra:
handleChange para texto, email, senha, select e checkboxonInvalid para interceptar a validação do navegador e exibir mensagens de erro personalizadasonReset para limpar o estado do formulário de volta aos valores iniciaischecked de checkbox versus value de input de texto em um único manipulador com narrowing de tipoonChange do React dispara a cada pressionamento de tecla para inputs de texto, o que difere do evento change nativo do DOM (que dispara no blur). Isso torna os inputs controlados reativos e habilita validação em tempo real.onSubmit dispara quando o formulário é enviado via tecla Enter (enquanto focado em um input) ou clicando em um botão type="submit". Sempre chame e.preventDefault() para tratamento no lado do cliente para evitar uma recarga completa da página.onInvalid dispara quando form.reportValidity() ou form.requestSubmit() aciona a validação e um input falha em suas restrições (required, minLength, pattern, etc.). Use e.preventDefault() para suprimir a tooltip do navegador e exibir UI personalizada.onReset dispara quando um botão type="reset" é clicado. Ele NÃO redefine o estado React -- você deve gerenciar a redefinição do estado por conta própria.Inputs controlados vs. não controlados:
// Controlado: O estado React é a fonte da verdade
function Controlled() {
const [value, setValue] = useState("");
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
// Não controlado: O DOM é a fonte da verdade
function Uncontrolled() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
console.log(inputRef.current?.value);
};
return <input ref={inputRef} defaultValue="" />;
}Extração de FormData (padrão moderno -- sem necessidade de estado controlado):
function FormDataExample() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// Obter campos individuais
const email = formData.get("email") as string;
// Converter para objeto simples
const data = Object.fromEntries(formData);
// Lidar com múltiplos valores (ex: multi-select, checkboxes com mesmo nome)
const tags = formData.getAll("tags") as string[];
console.log({ email, data, tags });
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
<select name="tags" multiple>
<option value="react">React</option>
<option value="typescript">TypeScript</option>
</select>
<button type="submit">Enviar</button>
</form>
);
}Ações de formulário React 19 com useActionState:
"use client";
import { useActionState } from "react";
type State = { message: string; errors?: Record<string, string> };
async function submitSignup(
prevState: State,
formData: FormData
): Promise<State> {
const email = formData.get("email") as string;
const password = formData.get("password") as string;
if (!email.includes("@")) {
return { message: "", errors: { email: "Email inválido" } };
}
if (password.length < 8) {
return { message: "", errors: { password: "Muito curto" } };
}
// Simular chamada de API
await new Promise((r) => setTimeout(r, 1000));
return { message: `Bem-vindo, ${email}!` };
}
export default function ActionForm() {
const [state, formAction, isPending] = useActionState(submitSignup, {
message: "",
});
return (
<form action={formAction}>
<input name="email" type="email" placeholder="Email" />
{state.errors?.email && <span>{state.errors.email}</span>}
<input name="password" type="password" placeholder="Senha" />
{state.errors?.password && <span>{state.errors.password}</span>}
<button type="submit" disabled={isPending}>
{isPending ? "Cadastrando..." : "Cadastrar"}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}Input de arquivo onChange:
function FileUpload() {
const [fileName, setFileName] = useState<string>("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setFileName(file.name);
console.log("Tamanho:", file.size, "Tipo:", file.type);
}
};
return (
<div>
<input type="file" onChange={handleChange} accept="image/*" />
{fileName && <p>Selecionado: {fileName}</p>}
</div>
);
}Select onChange com valores tipados:
type Color = "red" | "green" | "blue";
function ColorPicker() {
const [color, setColor] = useState<Color>("red");
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setColor(e.target.value as Color);
};
return (
<select value={color} onChange={handleChange}>
<option value="red">Vermelho</option>
<option value="green">Verde</option>
<option value="blue">Azul</option>
</select>
);
}// React.ChangeEvent<T> -- para manipuladores onChange
// T deve corresponder ao elemento: HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
e.target.value; // string -- o valor atual do input
e.target.name; // string -- o atributo name
e.target.type; // string -- "text", "checkbox", "email", etc.
e.target.checked; // boolean -- significativo apenas para checkboxes/radios
};
// React.FormEvent<HTMLFormElement> -- para onSubmit / onReset
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.currentTarget; // HTMLFormElement
new FormData(e.currentTarget); // FormData do formulário
};
// Tipando valores de FormData
const formData = new FormData(form);
const email = formData.get("email"); // FormDataEntryValue | null
const emailStr = formData.get("email") as string; // asserta para string
const file = formData.get("avatar") as File; // asserta para File
// Manipulador de união para múltiplos tipos de input
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
// Tipagem useActionState do React 19
const [state, action, isPending] = useActionState<State, FormData>(
submitAction,
initialState
);
// Tipando um formulário com Record para campos dinâmicos
type FormState = Record<string, string | boolean>;onChange do React NÃO é o evento change do DOM -- Em HTML nativo, change dispara no blur para inputs de texto. O onChange do React dispara a cada pressionamento de tecla, comportando-se como o evento input nativo. Isso surpreende desenvolvedores vindos do JavaScript puro. Se você precisa de comportamento apenas no blur, use onBlur em vez disso.
onReset não redefine o estado React -- Clicar em um botão type="reset" redefine os valores do formulário DOM para seus defaultValue, mas NÃO atualiza o estado React. Se você usa inputs controlados, os valores do estado sobrescrevem imediatamente a redefinição do DOM. Correção: Manipule onReset explicitamente e redefina seu estado para os valores iniciais.
onChange de Checkbox retorna e.target.checked, não e.target.value -- Para checkboxes, e.target.value é sempre o atributo value estático (padrão "on"). O estado real de alternância está em e.target.checked. Correção: Verifique e.target.type === "checkbox" e leia .checked para o estado booleano.
FormData.get() retorna FormDataEntryValue | null -- O tipo de retorno é string | File | null, não apenas string. Se você o passar diretamente para uma função que espera string, o TypeScript dará erro. Correção: Afirme o tipo: formData.get("email") as string.
e.currentTarget é null após operações assíncronas -- Assim como outros Eventos Sintéticos, acessar e.currentTarget dentro de um await ou setTimeout retorna null. Correção: Capture const form = e.currentTarget; antes de qualquer trabalho assíncrono, e então use new FormData(form).
Inputs de arquivo não podem ser controlados -- Definir value em um input de arquivo não é permitido por razões de segurança. Inputs de arquivo são sempre não controlados. Use onChange para ler o arquivo selecionado e armazená-lo no estado, mas não tente definir o valor do input.
Modelo mental diferente para useActionState do React 19 -- A função de ação recebe (prevState, formData) e retorna o novo estado. Não há e.preventDefault() -- o formulário usa a prop action em vez de onSubmit. Misturar onSubmit e action no mesmo formulário leva a um comportamento confuso. Correção: Escolha um padrão por formulário: ou onSubmit com preventDefault, ou action com useActionState.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
Prop action do React 19 + useActionState | Formulários validados no servidor, aprimoramento progressivo, estados pendentes | Você precisa de controle granular no lado do cliente a cada pressionamento de tecla |
| React Hook Form | Formulários complexos com muitos campos, validação profunda, sensível ao desempenho | Formulários simples com 1-3 campos |
| Zod + react-hook-form | Validação baseada em schema compartilhada entre cliente e servidor | A validação é trivial (apenas required) |
| Formik | Projetos legados que já o utilizam | Novos projetos (prefira React Hook Form ou nativo) |
Inputs não controlados + FormData | Formulários simples onde você só precisa de valores no submit | Você precisa de validação em tempo real ou estado derivado de inputs |
| Server Actions (Next.js) | Envio de formulário que executa lógica do lado do servidor diretamente | Aplicativos apenas do lado do cliente sem servidor |
O onChange do React dispara a cada pressionamento de tecla para inputs de texto, comportando-se como o evento input nativo. O evento change nativo do DOM dispara apenas no blur. Esta é uma fonte comum de confusão para desenvolvedores vindos do JavaScript puro.
value e o atualiza via onChange.defaultValue e lê via ref.const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
console.log(data); // { email: "...", password: "..." }
};Sem e.preventDefault(), o navegador executa seu comportamento padrão de envio de formulário, que causa uma recarga completa da página. Para tratamento no lado do cliente no React, você sempre precisa prevenir esse padrão.
O botão type="reset" redefine os valores do formulário DOM para seus defaultValue, mas NÃO atualiza o estado React. Como os inputs controlados sobrescrevem imediatamente o DOM com os valores do estado, a redefinição parece não fazer nada. Manipule onReset explicitamente e redefina seu estado para os valores iniciais.
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, type } = e.target;
const value = type === "checkbox"
? e.target.checked
: e.target.value;
setFormData((prev) => ({ ...prev, [name]: value }));
};Verifique e.target.type === "checkbox" e leia .checked em vez de .value.
useActionState usa a prop action em <form> em vez de onSubmit(prevState, formData) e retorna o novo estadoisPending para estados de carregamentoonSubmit e action no mesmo formulárioOs eventos sintéticos do React são reciclados após o retorno do manipulador. Acessar e.currentTarget dentro de um await ou setTimeout retorna null. Capture-o primeiro: const form = e.currentTarget; e então use new FormData(form).
<input
required
onInvalid={(e) => {
e.preventDefault(); // suprime a tooltip do navegador
setError(e.currentTarget.validationMessage);
}}
/>Inputs de arquivo não podem ser controlados por razões de segurança. O navegador proíbe a definição de value em <input type="file">. Eles são sempre não controlados. Use onChange para ler o arquivo selecionado e armazená-lo no estado.
FormData.get() retorna FormDataEntryValue | null, que é string | File | null. Se você passá-lo para uma função que espera string, o TypeScript dará erro. Afirme o tipo: formData.get("email") as string.
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥