Eventos de Formularios
Maneja envíos de formularios, cambios de entrada y restablecimientos de formularios en componentes de React.
Busca en todas las páginas de la documentación
Maneja envíos de formularios, cambios de entrada y restablecimientos de formularios en componentes de React.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| Propiedad de React | Tipo TypeScript | Se dispara cuando | Notas |
|---|---|---|---|
onChange | React.ChangeEvent<T> | El valor de un input, select o textarea cambia | En React, se dispara en cada pulsación de tecla (no solo en blur) |
onInput | React.FormEvent<T> | El valor de un input cambia | Casi idéntico a onChange en React -- prefiere onChange |
onSubmit | React.FormEvent<HTMLFormElement> | Se envía el formulario (tecla Enter o botón submit) | Llama a e.preventDefault() para manejo en el cliente |
onReset | React.FormEvent<HTMLFormElement> | Se hace clic en el botón de restablecimiento del formulario | Raramente usado -- la mayoría de aplicaciones manejan el restablecimiento mediante state |
onInvalid | React.FormEvent<T> | Falla la validación integrada en un input | Se dispara antes de que el navegador muestre su tooltip de validación |
Tarjeta de referencia rápida -- lista para copiar y pegar.
// Input controlado con onChange
function ControlledInput() {
const [name, setName] = React.useState("");
return (
<input
value={name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)}
placeholder="Ingresa el nombre"
/>
);
}
// Envío de formulario con 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>
);
}
// Extracción de FormData sin 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">Inicia sesión</button>
</form>
);
}Cuándo usarlo: Necesitas manejar la entrada del usuario en formularios -- campos de texto, selects, casillas de verificación, cargas de archivos o envíos de formularios con validación.
"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 = "Al menos 3 caracteres";
if (!data.email.includes("@")) errors.email = "Dirección de correo no válida";
if (data.password.length < 8) errors.password = "Al menos 8 caracteres";
if (!data.role) errors.role = "Selecciona un rol";
if (!data.agree) errors.agree = "Debes aceptar los términos";
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 }));
// Limpia el error de este campo al cambiar
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("Datos de registro:", formData);
setSubmitted(true);
},
[formData]
);
const handleReset = useCallback(() => {
setFormData(INITIAL_STATE);
setErrors({});
setSubmitted(false);
}, []);
const handleInvalid = useCallback(
(e: React.FormEvent<HTMLInputElement>) => {
e.preventDefault(); // Previene el tooltip del navegador
const name = e.currentTarget.name as keyof FormData;
setErrors((prev) => ({
...prev,
[name]: e.currentTarget.validationMessage,
}));
},
[]
);
if (submitted) {
return (
<div style={{ padding: 24 }}>
<p>Cuenta creada para {formData.username}.</p>
<button onClick={handleReset}>Registra otro</button>
</div>
);
}
return (
<form
onSubmit={handleSubmit}
onReset={handleReset}
noValidate
style={{
maxWidth: 400,
display: "flex",
flexDirection: "column",
gap: 16,
padding: 24,
}}
>
<h2 style={{ margin: 0 }}>Registrarse</h2>
<div>
<label htmlFor="username">Nombre de usuario</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">Correo</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">Contraseña</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">Rol</label>
<select
id="role"
name="role"
value={formData.role}
onChange={handleChange}
style={{ display: "block", width: "100%", padding: "8px" }}
>
<option value="">Selecciona un rol...</option>
<option value="developer">Desarrollador</option>
<option value="designer">Diseñador</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}
/>{" "}
Acepto los términos
</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" }}>
Crear Cuenta
</button>
<button type="reset" style={{ padding: "8px 16px" }}>
Restablecer
</button>
</div>
</form>
);
}Lo que esto demuestra:
handleChange para texto, correo, contraseña, select y casilla de verificaciónonInvalid para interceptar la validación del navegador y mostrar mensajes de error personalizadosonReset para limpiar el estado del formulario a los valores inicialeschecked en casillas de verificación vs value en inputs de texto en un solo manejador con narrowing de tiposonChange de React se dispara en cada pulsación de tecla para inputs de texto, lo que difiere del evento change del DOM nativo (que se dispara en blur). Esto hace que los inputs controlados sean reactivos y permite validación en tiempo real.onSubmit se dispara cuando se envía el formulario a través de la tecla Enter (mientras se enfoca en un input) o haciendo clic en un botón type="submit". Siempre llama a e.preventDefault() para manejo en el cliente para evitar una recarga de página completa.onInvalid se dispara cuando form.reportValidity() o form.requestSubmit() dispara validación y un input falla sus restricciones (required, minLength, pattern, etc.). Usa e.preventDefault() para suprimir el tooltip del navegador y mostrar una interfaz personalizada.onReset se dispara cuando se hace clic en un botón type="reset". NO restablece el estado de React -- debes manejar el restablecimiento de estado por ti mismo.Inputs controlados vs no controlados:
// Controlado: el estado de React es la fuente de verdad
function Controlled() {
const [value, setValue] = useState("");
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
// No controlado: el DOM es la fuente de verdad
function Uncontrolled() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
console.log(inputRef.current?.value);
};
return <input ref={inputRef} defaultValue="" />;
}Extracción de FormData (patrón moderno -- sin necesidad de estado controlado):
function FormDataExample() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// Obtén campos individuales
const email = formData.get("email") as string;
// Convierte a objeto plano
const data = Object.fromEntries(formData);
// Maneja múltiples valores (p.ej., multi-select, casillas de verificación con el mismo nombre)
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>
);
}Acciones de formulario de React 19 con 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: "Correo no válido" } };
}
if (password.length < 8) {
return { message: "", errors: { password: "Muy corto" } };
}
// Simula una llamada a la API
await new Promise((r) => setTimeout(r, 1000));
return { message: `¡Bienvenida, ${email}!` };
}
export default function ActionForm() {
const [state, formAction, isPending] = useActionState(submitSignup, {
message: "",
});
return (
<form action={formAction}>
<input name="email" type="email" placeholder="Correo" />
{state.errors?.email && <span>{state.errors.email}</span>}
<input name="password" type="password" placeholder="Contraseña" />
{state.errors?.password && <span>{state.errors.password}</span>}
<button type="submit" disabled={isPending}>
{isPending ? "Registrando..." : "Registrarse"}
</button>
{state.message && <p>{state.message}</p>}
</form>
);
}onChange de entrada de archivo:
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("Tamaño:", file.size, "Tipo:", file.type);
}
};
return (
<div>
<input type="file" onChange={handleChange} accept="image/*" />
{fileName && <p>Seleccionado: {fileName}</p>}
</div>
);
}Select onChange con valores tipificados:
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">Rojo</option>
<option value="green">Verde</option>
<option value="blue">Azul</option>
</select>
);
}// React.ChangeEvent<T> -- para manejadores onChange
// T debe coincidir con el elemento: HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
e.target.value; // string -- el valor actual del input
e.target.name; // string -- el atributo name
e.target.type; // string -- "text", "checkbox", "email", etc.
e.target.checked; // boolean -- solo significativo para casillas de verificación/radios
};
// React.FormEvent<HTMLFormElement> -- para onSubmit / onReset
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.currentTarget; // HTMLFormElement
new FormData(e.currentTarget); // FormData del formulario
};
// Tipificación de valores de FormData
const formData = new FormData(form);
const email = formData.get("email"); // FormDataEntryValue | null
const emailStr = formData.get("email") as string; // afirma a string
const file = formData.get("avatar") as File; // afirma a File
// Manejador de unión para múltiples tipos de input
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
// Tipificación de React 19 useActionState
const [state, action, isPending] = useActionState<State, FormData>(
submitAction,
initialState
);
// Tipificación de un formulario con Record para campos dinámicos
type FormState = Record<string, string | boolean>;El onChange de React NO es el evento change del DOM -- En HTML nativo, change se dispara en blur para inputs de texto. El onChange de React se dispara en cada pulsación de tecla, comportándose como el evento nativo input. Esto sorprende a los desarrolladores que vienen de JS vanilla. Si necesitas comportamiento solo en blur, usa onBlur en su lugar.
onReset no restablece el estado de React -- Hacer clic en un botón type="reset" restablece los valores del formulario DOM a su defaultValue pero NO actualiza el estado de React. Si usas inputs controlados, los valores de estado inmediatamente sobrescriben el restablecimiento del DOM. Solución: Maneja onReset explícitamente y restablece tu estado a valores iniciales.
onChange de casilla de verificación te da e.target.checked, no e.target.value -- Para casillas de verificación, e.target.value es siempre el atributo value estático (por defecto "on"). El estado de alternancia real está en e.target.checked. Solución: Verifica e.target.type === "checkbox" y lee .checked para estado booleano.
FormData.get() devuelve FormDataEntryValue | null -- El tipo de retorno es string | File | null, no solo string. Si lo pasas directamente a una función que espera string, TypeScript lanzará un error. Solución: Afirma el tipo: formData.get("email") as string.
e.currentTarget es null después de operaciones asincrónicas -- Al igual que otros Eventos Sintéticos, acceder a e.currentTarget dentro de un await o setTimeout da null. Solución: Captura const form = e.currentTarget; antes de cualquier trabajo asincrónico, luego usa new FormData(form).
Los inputs de archivo no pueden ser controlados -- Establecer value en un input de archivo no está permitido por razones de seguridad. Los inputs de archivo son siempre no controlados. Usa onChange para leer el archivo seleccionado y almacenarlo en state, pero no intentes establecer el valor del input.
El useActionState de React 19 requiere un modelo mental diferente -- La función de acción recibe (prevState, formData) y devuelve el nuevo estado. No hay e.preventDefault() -- el formulario usa la propiedad action en lugar de onSubmit. Mezclar onSubmit y action en el mismo formulario lleva a un comportamiento confuso. Solución: Elige un patrón por formulario: ya sea onSubmit con preventDefault, o action con useActionState.
| Alternativa | Úsalo cuando | No lo uses cuando |
|---|---|---|
Propiedad action de React 19 + useActionState | Formularios validados por servidor, mejora progresiva, estados pendientes | Necesitas control granular en el cliente de cada pulsación de tecla |
| React Hook Form | Formularios complejos con muchos campos, validación profunda, sensibles al rendimiento | Formularios simples con 1-3 campos |
| Zod + react-hook-form | Validación basada en esquema compartida entre cliente y servidor | La validación es trivial (solo required) |
| Formik | Proyectos heredados que ya lo usan | Nuevos proyectos (prefiere React Hook Form o nativa) |
Inputs no controlados + FormData | Formularios simples donde solo necesitas valores al enviar | Necesitas validación en tiempo real o estado derivado de inputs |
| Acciones de Servidor (Next.js) | Envío de formulario que ejecuta lógica en el lado del servidor directamente | Aplicaciones solo cliente sin servidor |
El onChange de React se dispara en cada pulsación de tecla para inputs de texto, comportándose como el evento nativo input. El evento change del DOM nativo solo se dispara en blur. Esta es una fuente común de confusión para desarrolladores que vienen de JS vanilla.
value y lo actualizas a través de onChange.defaultValue y lees a través de un ref.const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
console.log(data); // { email: "...", password: "..." }
};Sin e.preventDefault(), el navegador realiza su comportamiento predeterminado de envío de formulario, que causa una recarga de página completa. Para manejo en el cliente en React, siempre necesitas prevenir esto.
El botón type="reset" restablece los valores del formulario DOM a su defaultValue, pero NO actualiza el estado de React. Dado que los inputs controlados inmediatamente sobrescriben el DOM con valores de estado, el restablecimiento parece que no hace nada. Maneja onReset explícitamente y restablece tu estado a valores iniciales.
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 }));
};Verifica e.target.type === "checkbox" y lee .checked en lugar de .value.
useActionState usa la propiedad action en <form> en lugar de onSubmit(prevState, formData) y devuelve el nuevo estadoisPending para estados de cargaonSubmit y action en el mismo formularioLos eventos sintéticos de React se reciclan después de que el manejador devuelve. Acceder a e.currentTarget dentro de un await o setTimeout da null. Captura primero: const form = e.currentTarget; luego usa new FormData(form).
<input
required
onInvalid={(e) => {
e.preventDefault(); // suprime tooltip del navegador
setError(e.currentTarget.validationMessage);
}}
/>Los inputs de archivo no pueden ser controlados por razones de seguridad. El navegador prohíbe establecer value en <input type="file">. Son siempre no controlados. Usa onChange para leer el archivo seleccionado y almacenarlo en state.
FormData.get() devuelve FormDataEntryValue | null, que es string | File | null. Si lo pasas a una función que espera string, TypeScript lanzará un error. Afirma el 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 actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥