Formularios
Maneja la entrada del usuario con componentes controlados, refs no controlados o acciones de formulario de React 19.
Busca en todas las páginas de la documentación
Maneja la entrada del usuario con componentes controlados, refs no controlados o acciones de formulario de React 19.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
// Input controlado
const [email, setEmail] = useState("");
<input value={email} onChange={e => setEmail(e.target.value)} />
// Input no controlado con ref
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} defaultValue="" />
// Léelo después: inputRef.current?.value
// Acción de formulario de React 19
async function createUser(formData: FormData) {
const name = formData.get("name") as string;
await saveToDatabase(name);
}
<form action={createUser}>
<input name="name" required />
<button type="submit">Crear</button>
</form>
// useActionState de React 19
const [state, formAction, isPending] = useActionState(submitFn, initialState);Cuándo recurrir a esto: cada vez que recopilas entrada del usuario - formularios de inicio de sesión, barras de búsqueda, páginas de configuración, asistentes de varios pasos.
"use client";
import { useState, useActionState } from "react";
// --- Formulario controlado ---
export function ControlledSignup() {
const [form, setForm] = useState({ name: "", email: "", role: "viewer" });
const [submitted, setSubmitted] = useState(false);
function updateField(field: string, value: string) {
setForm(prev => ({ ...prev, [field]: value }));
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setSubmitted(true);
}
if (submitted) {
return (
<div className="rounded bg-green-50 p-4 text-green-800">
¡Bienvenido, {form.name}! Enviamos una confirmación a {form.email}.
</div>
);
}
return (
<form onSubmit={handleSubmit} className="max-w-sm space-y-3 rounded border p-4">
<div>
<label htmlFor="name" className="block text-sm font-medium">Nombre</label>
<input
id="name"
value={form.name}
onChange={e => updateField("name", e.target.value)}
required
className="w-full rounded border px-3 py-1"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium">Correo electrónico</label>
<input
id="email"
type="email"
value={form.email}
onChange={e => updateField("email", e.target.value)}
required
className="w-full rounded border px-3 py-1"
/>
</div>
<div>
<label htmlFor="role" className="block text-sm font-medium">Rol</label>
<select
id="role"
value={form.role}
onChange={e => updateField("role", e.target.value)}
className="w-full rounded border px-3 py-1"
>
<option value="viewer">Visor</option>
<option value="editor">Editor</option>
<option value="admin">Administrador</option>
</select>
</div>
<button type="submit" className="rounded bg-blue-600 px-4 py-2 text-white">
Registrarse
</button>
</form>
);
}
// --- Acción de formulario de React 19 con useActionState ---
interface FormState {
message: string;
error: boolean;
}
async function submitFeedback(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const feedback = formData.get("feedback") as string;
// Simula el retardo del servidor
await new Promise(resolve => setTimeout(resolve, 1000));
if (feedback.length < 10) {
return { message: "Los comentarios deben tener al menos 10 caracteres.", error: true };
}
return { message: `¡Gracias por tus comentarios!`, error: false };
}
export function FeedbackForm() {
const [state, formAction, isPending] = useActionState(submitFeedback, {
message: "",
error: false,
});
return (
<form action={formAction} className="max-w-sm space-y-3 rounded border p-4">
<label htmlFor="feedback" className="block text-sm font-medium">
Tus comentarios
</label>
<textarea
id="feedback"
name="feedback"
required
rows={3}
className="w-full rounded border px-3 py-1"
placeholder="Dinos qué piensas..."
/>
{state.message && (
<p className={state.error ? "text-sm text-red-600" : "text-sm text-green-600"}>
{state.message}
</p>
)}
<button
type="submit"
disabled={isPending}
className="rounded bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
>
{isPending ? "Enviando..." : "Enviar comentarios"}
</button>
</form>
);
}Qué demuestra esto:
value + onChange para cada inputuseActionState (React 19) gestionando el estado del envío del formulario, el indicador de pendiente y la validaciónFormData para leer los valores del formulario sin estado controladolabel + htmlForvalue en cada renderFormDataasync a <form action={fn}>. React gestiona el envío, proporciona isPending e integra con useActionState para manejar los valores de retornouseActionState(actionFn, initialState) devuelve [state, wrappedAction, isPending] - llama a actionFn(prevState, formData) al enviar y actualiza el estado con el resultado| Enfoque | Fuente de la Verdad | Ideal Para |
|---|---|---|
Controlado (value + onChange) | Estado de React | Validación en tiempo real, campos condicionales, inputs interdependientes complejos |
No controlado (defaultValue + ref) | DOM | Formularios simples, integraciones de terceros, inputs sensibles al rendimiento |
| Acciones de formulario (React 19) | FormData | Mutaciones del lado del servidor, mejora progresiva, reducción del estado del cliente |
useActionState:
| Parámetro | Tipo | Descripción |
|---|---|---|
action | (prevState: T, formData: FormData) => T or Promise<T> | Función llamada al enviar el formulario |
initialState | T | Estado inicial antes del primer envío |
permalink? | string | URL opcional para mejora progresiva con SSR |
| Retorno | Tipo | Descripción |
|---|---|---|
state | T | Estado actual (actualizado tras completarse la acción) |
formAction | (formData: FormData) => void | Acción envuelta para pasar a <form action> |
isPending | boolean | true mientras la acción se está ejecutando |
No controlado con FormData (sin necesidad de useState):
function SearchForm() {
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const query = formData.get("query") as string;
router.push(`/search?q=${encodeURIComponent(query)}`);
}
return (
<form onSubmit={handleSubmit}>
<input name="query" defaultValue="" />
<button type="submit">Buscar</button>
</form>
);
}useFormStatus para botones de envío anidados:
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Guardando..." : "Guardar"}
</button>
);
}
// Uso - SubmitButton debe ser un hijo de un <form>
<form action={saveAction}>
<input name="title" />
<SubmitButton />
</form>Actualizaciones optimistas con useOptimistic:
import { useOptimistic } from "react";
function MessageList({ messages, sendAction }: Props) {
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newMessage: string) => [
...state,
{ id: "temp", text: newMessage, sending: true },
]
);
async function handleSubmit(formData: FormData) {
const text = formData.get("message") as string;
addOptimistic(text);
await sendAction(formData);
}
return (
<form action={handleSubmit}>
<ul>
{optimisticMessages.map(msg => (
<li key={msg.id} style={{ opacity: msg.sending ? 0.5 : 1 }}>
{msg.text}
</li>
))}
</ul>
<input name="message" />
<button type="submit">Enviar</button>
</form>
);
}// Tipar onChange para diferentes tipos de input
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { ... }
function handleSelectChange(e: React.ChangeEvent<HTMLSelectElement>) { ... }
function handleTextareaChange(e: React.ChangeEvent<HTMLTextAreaElement>) { ... }
// Tipar el estado de la acción de formulario
interface ActionState {
success: boolean;
errors: Record<string, string>;
}
async function myAction(
prev: ActionState,
formData: FormData
): Promise<ActionState> {
// valida y devuelve el nuevo estado
}Falta value u onChange - Establecer value sin onChange hace que el input sea de solo lectura. React advierte sobre esto. Solución: añade un manejador onChange o usa defaultValue para inputs no controlados.
defaultValue no se actualiza - Cambiar defaultValue después del montaje no tiene efecto, porque solo establece el valor inicial del DOM. Solución: usa value controlado si necesitas que React gestione las actualizaciones, o añade una key para volver a montar el input.
checked de casillas y radios - Estos usan checked / defaultChecked, no value / defaultValue. Solución: <input type="checkbox" checked={isOn} onChange={e => setIsOn(e.target.checked)} />.
Los inputs numéricos devuelven strings - e.target.value siempre es un string, incluso para <input type="number">. Solución: conviértelo de forma explícita: Number(e.target.value) o parseInt(e.target.value, 10).
useFormStatus fuera de un formulario - useFormStatus solo funciona cuando el componente se renderiza como descendiente de un <form>. Llamarlo en el mismo componente que renderiza el <form> devuelve datos obsoletos. Solución: extrae el botón de envío a un componente hijo.
| Alternativa | Úsalo Cuando | No lo Uses Cuando |
|---|---|---|
| React Hook Form | Validación compleja, muchos campos, formularios críticos para el rendimiento | Formularios simples con 1-3 campos |
| Zod + Server Actions | Mutaciones del servidor validadas por esquema en Next.js | Formularios solo de cliente sin servidor |
| Formik | Proyectos heredados que ya lo usan | Proyectos nuevos (React Hook Form es más ligero) |
<dialog> nativo + <form method="dialog"> | Diálogos de confirmación modales | Recopilación de datos de varios campos |
value + onChange - React es la fuente de la verdaddefaultValue - léelo bajo demanda con un ref o FormDataPuedes pasar una función async directamente a <form action={fn}>. React la llama con un objeto FormData al enviar, gestiona el estado pendiente e integra con useActionState para manejar los valores de retorno - sin necesidad de e.preventDefault().
useActionState(actionFn, initialState) devuelve [state, formAction, isPending]. Al enviar, llama a actionFn(prevState, formData) y actualiza state con el resultado. isPending es true mientras la acción se ejecuta.
Establecer value sin un manejador onChange hace que el input no sea editable - React fija el valor al estado. Añade onChange para actualizar el estado, o cambia a defaultValue para un input no controlado.
defaultValue solo establece el valor inicial del DOM al montar. Los cambios posteriores al montaje no tienen efecto. Usa value controlado si React necesita gestionar las actualizaciones, o añade una prop key para forzar el remontaje.
Usa checked / defaultChecked en lugar de value / defaultValue:
<input
type="checkbox"
checked={isEnabled}
onChange={e => setIsEnabled(e.target.checked)}
/>e.target.value siempre es un string, incluso para <input type="number">. Conviértelo de forma explícita: Number(e.target.value) o parseInt(e.target.value, 10).
useFormStatus() devuelve { pending } para mostrar el estado de carga durante una acción de formulario. Debe llamarse en un componente que sea hijo de un <form> - no en el mismo componente que renderiza el formulario.
Usa un único objeto de estado y una función de actualización genérica:
const [form, setForm] = useState({ name: "", email: "" });
function updateField(field: string, value: string) {
setForm(prev => ({ ...prev, [field]: value }));
}Usa useOptimistic para mostrar inmediatamente el resultado esperado mientras la acción se procesa. Si la acción falla, React revierte automáticamente al estado real.
<label htmlFor="id"> que coincida con el id del inputrequired, aria-describedby para los mensajes de error y aria-invalid para los campos inválidosRevisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥