Cómo persistir el estado de un formulario en el navegador
Pasas diez minutos rellenando una solicitud de empleo de varios pasos. Pulsas el botón “atrás” por accidente. Todo se ha perdido.
Este es uno de los fallos de UX más frustrantes en el desarrollo web, y se puede evitar por completo. A continuación te explicamos cómo persistir el estado de un formulario en el navegador utilizando el mecanismo de almacenamiento adecuado para cada situación.
Puntos clave
- Las aplicaciones de una sola página (SPA) a menudo pierden los datos del formulario al navegar, porque el DOM puede volver a renderizarse desde cero en lugar de restaurarse desde la caché.
- Elige tu almacenamiento según las necesidades de duración:
localStoragepara borradores a largo plazo,sessionStoragepara sesiones de una sola pestaña e IndexedDB para datos grandes o estructurados. - El patrón básico de autoguardado es sencillo: aplica debounce a los eventos de input, guarda en el almacenamiento, restaura al montar y limpia tras un envío exitoso.
- Nunca almacenes contraseñas, tokens o datos de pago en Web Storage, ya que son vulnerables a ataques XSS.
- Envuelve siempre las escrituras en try/catch para manejar
QuotaExceededErrory el almacenamiento particionado de forma elegante.
Por qué los datos del formulario desaparecen en las aplicaciones modernas
Las páginas tradicionales renderizadas en el servidor tienen una ligera ventaja aquí. Los navegadores suelen restaurar los valores del formulario al navegar hacia atrás porque la propia página está cacheada. Las aplicaciones de una sola página no siempre disfrutan de esa ventaja. Cuando tu JavaScript vuelve a renderizar un formulario desde cero, es posible que el navegador no tenga un DOM cacheado que restaurar, por lo que los campos vuelven vacíos.
La solución es guardar tú mismo el estado del formulario en el almacenamiento del navegador y restaurarlo cuando el formulario se monte.
Elegir el mecanismo de almacenamiento adecuado
No todos los problemas de persistencia necesitan la misma solución. Aquí tienes una comparación práctica:
| Método | Persiste hasta | Aislado por pestaña | Límite de tamaño | Mejor para |
|---|---|---|---|---|
localStorage | Borrado manual | No | ~5–10 MB | Borradores a largo plazo |
sessionStorage | Cierre de la pestaña | Sí | ~5–10 MB | Formularios de una sola sesión |
IndexedDB | Borrado manual | No | Depende del navegador | Datos grandes o estructurados |
| Estado del History API | Entrada de navegación | Sí | Objetos pequeños | Navegación atrás/adelante en SPA |
localStorage funciona bien para borradores que quieres que sobrevivan a los reinicios del navegador, como un editor de entradas de blog o un formulario de registro largo. sessionStorage es mejor cuando solo necesitas que los datos sobrevivan a una recarga dentro de la misma pestaña, no entre pestañas. IndexedDB tiene sentido para borradores cuando almacenas contenido enriquecido, metadatos de archivos u objetos anidados complejos que serían difíciles de manejar como cadenas JSON.
Tanto localStorage como sessionStorage son síncronos y basados en cadenas, lo que significa que cada lectura y escritura bloquea el hilo principal y requiere JSON.stringify/JSON.parse. IndexedDB es asíncrono y maneja datos estructurados de forma nativa, lo que lo hace más adecuado para cualquier cosa que vaya más allá de pares clave-valor simples. Los tres tienen buen soporte en los navegadores modernos.
Discover how at OpenReplay.com.
Implementar autoguardado con localStorage
El patrón básico es sencillo: guardar al introducir datos, restaurar al cargar, limpiar tras un envío exitoso.
const DRAFT_KEY = 'contact_form_draft';
const form = document.querySelector('#contact-form');
// Autosave with debounce
let saveTimer;
form.addEventListener('input', (e) => {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
const formData = Object.fromEntries(new FormData(e.currentTarget));
try {
localStorage.setItem(DRAFT_KEY, JSON.stringify(formData));
} catch (err) {
if (err.name === 'QuotaExceededError') {
console.warn('Storage full, draft not saved');
}
}
}, 500);
});
// Restore on load
window.addEventListener('DOMContentLoaded', () => {
const saved = localStorage.getItem(DRAFT_KEY);
if (!saved) return;
try {
const draft = JSON.parse(saved);
Object.entries(draft).forEach(([name, value]) => {
const field = form.querySelector(`[name="${name}"]`);
if (field) field.value = value;
});
} catch {
localStorage.removeItem(DRAFT_KEY);
}
});
// Clear after successful submit
form.addEventListener('submit', () => {
localStorage.removeItem(DRAFT_KEY);
});
Aplicar debounce a la llamada de guardado (500 ms en este caso) evita saturar el almacenamiento con cada pulsación de tecla. Envuelve siempre setItem en un try/catch: los navegadores pueden lanzar (y lanzan) QuotaExceededError, especialmente en entornos con poco almacenamiento o cuando el almacenamiento de terceros está particionado. También es prudente envolver JSON.parse en un try/catch, ya que un borrador corrupto podría lanzar una excepción y romper el paso de restauración.
Qué no almacenar
Nunca persistas contraseñas, números de tarjetas de pago, tokens de autenticación ni datos personales sensibles en localStorage o sessionStorage. Estas APIs son accesibles para cualquier JavaScript que se ejecute en la página, lo que las hace vulnerables a ataques XSS. Si tu formulario recopila campos sensibles, exclúyelos por completo de tu lógica de borrador.
También conviene saber esto: en contextos embebidos o de terceros, los navegadores cada vez particionan o restringen más el acceso a Web Storage. No des por hecho que el almacenamiento siempre está disponible: compruébalo y maneja los fallos con elegancia.
Limpieza de borradores y casos límite
Elimina siempre el borrador tras un envío exitoso del formulario. Los borradores obsoletos que reaparecen de forma inesperada confunden a los usuarios. Si la estructura de tu formulario cambia con el tiempo, considera versionar tu clave de almacenamiento (por ejemplo, contact_form_draft_v2) para que los borradores antiguos no provoquen errores silenciosos al restaurar.
Conclusión
La persistencia de formularios en el navegador no requiere ni librería ni backend. Una pequeña cantidad de JavaScript bien pensado (guardados con debounce, restauraciones seguras y limpieza tras el envío) basta para evitar la pérdida de datos y hacer que tus formularios se sientan notablemente más fiables.
Preguntas frecuentes
Usa localStorage cuando quieras que el borrador sobreviva a los reinicios del navegador, como en formularios largos, editores de blog o solicitudes de varios pasos a las que los usuarios puedan volver días después. Usa sessionStorage cuando el borrador solo necesite sobrevivir a una recarga accidental dentro de la misma pestaña y deba desaparecer al cerrarla. Ambos comparten la misma API, por lo que cambiar entre ellos es una modificación de una sola línea.
Los inputs de archivos no pueden restaurarse programáticamente por razones de seguridad. El navegador no te permitirá establecer el valor de un input de tipo file. Si los usuarios suben archivos, almacena los metadatos del archivo o súbelo inmediatamente al servidor y persiste el ID de referencia resultante. Para archivos más grandes mantenidos en el cliente, usa IndexedDB para almacenar directamente el objeto File o Blob hasta el envío.
Para formularios típicos, no. Las escrituras en localStorage son rápidas para cargas pequeñas, y aplicar debounce a los eventos de input a unos 500 milisegundos mantiene las escrituras poco frecuentes. Los problemas aparecen cuando los borradores crecen mucho o cuando los guardados se ejecutan con cada pulsación de tecla sin debounce, ya que cada escritura bloquea el hilo principal. Para datos grandes o estructurados, cambia a IndexedDB, que es asíncrono y no bloqueante.
Escucha el evento storage en el objeto window. Se dispara en otras pestañas cada vez que localStorage cambia en una pestaña, dándote la clave y el nuevo valor. Luego puedes actualizar los campos del formulario en las pestañas que escuchan según corresponda. Ten en cuenta que el evento storage no se dispara en la pestaña que realizó el cambio, solo en otras pestañas que estén viendo el mismo origen.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.