JavaScript History API
En el desarrollo web moderno, la JavaScript History API te permite manipular el historial del navegador sin recargar la página.
Introducción a la JavaScript History API
En el desarrollo web moderno, crear experiencias de usuario fluidas implica con frecuencia manipular el historial del navegador. La JavaScript History API te permite leer y modificar el historial de sesión del navegador — la lista de páginas (y estados de URL) que el usuario ha visitado en la pestaña actual. Con esta API, los desarrolladores pueden actualizar la barra de direcciones y la pila de navegación hacia atrás/adelante sin provocar una recarga completa de la página, que es exactamente lo que las aplicaciones de una sola página (SPAs) necesitan para sentirse rápidas y nativas.
Esta página cubre los tres métodos principales (pushState, replaceState y los helpers de navegación), cómo reaccionar al evento popstate, los errores comunes que suelen confundir a los desarrolladores y las buenas prácticas para usar la API en producción.
Por qué existe la History API
Antes de esta API, la única forma de cambiar la URL era navegar, lo que recargaba el documento completo. Las SPAs renderizan nuevas "páginas" con JavaScript, por lo que necesitan que la URL se mantenga sincronizada sin una recarga — de lo contrario, el botón de retroceso, los marcadores y los enlaces compartibles dejan de funcionar.
La History API soluciona esto dándote una forma de:
- Añadir una nueva entrada a la pila de navegación hacia atrás/adelante (
pushState). - Modificar la entrada actual en su lugar (
replaceState). - Reaccionar cuando el usuario se mueve por esa pila con los botones de retroceso/avance (
popstate).
Puedes leer los datos de la entrada actual mediante la propiedad de solo lectura history.state, y history.length indica cuántas entradas hay en la pila de sesión. Para construir las URLs que pasas a estos métodos, el objeto URL es un complemento muy útil.
El object history de un vistazo
La API se expone en el objeto global window.history (puedes escribir simplemente history):
| Miembro | Qué hace |
|---|---|
history.pushState(state, title, url) | Añade una nueva entrada a la pila de historial y actualiza la URL. |
history.replaceState(state, title, url) | Reemplaza la entrada actual — no se crea una nueva entrada en la pila. |
history.state | Copia de solo lectura del object de estado de la entrada actual. |
history.length | Número de entradas en el historial de sesión. |
history.back() | Retrocede una entrada (equivalente al botón de retroceso del navegador). |
history.forward() | Avanza una entrada. |
history.go(n) | Salta n entradas (negativo = retroceder, positivo = avanzar). |
Uso de la History API en aplicaciones web
Navegación entre estados
La History API permite navegar entre diferentes estados de una aplicación sin recargar la página. pushState() recibe tres argumentos — un object state (cualquier valor serializable), un title (ignorado por la mayoría de los navegadores, así que pasa una string vacía) y una url (resuelta de forma relativa a la página actual y debe tener el mismo origen). Así es como se empuja un nuevo estado:
<div>
<button onclick="changeState()">Go to New State</button>
</div>
<script>
// Function to change state
function changeState() {
const newState = { id: 'newState' };
// Push a new state to the history stack
window.history.pushState(newState, 'New State', 'new-state-url');
}
</script>Esto añade un nuevo estado a la pila de historial usando pushState(). Observa lo que no hace: no carga new-state-url y no dispara un evento popstate. pushState solo actualiza la barra de direcciones y la pila — renderizar el contenido correspondiente es tu responsabilidad.
Manejo del evento Popstate
Cuando el usuario hace clic en el botón de retroceso o avance del navegador (o cuando llamas a history.back() / history.forward()), se dispara el evento popstate. La propiedad state del evento contiene el object de estado que pasaste previamente a pushState/replaceState para la entrada que se activa en ese momento. Manéjalo para restaurar la vista correcta:
window.addEventListener('popstate', function(event) {
if(event.state) {
console.log('State changed:', event.state);
// Handle the state object here
}
});Este listener reacciona a los eventos popstate, registrando los cambios y permitiendo ajustes de estado en función del historial de navegación del usuario. Cuando event.state es null, el usuario ha navegado de vuelta a una entrada creada por una carga de página normal (una que nunca recibió un object de estado), así que renderiza tu vista predeterminada.
Reemplazar el estado actual
A veces quieres actualizar la entrada actual sin añadir un nuevo registro a la pila — por ejemplo, sincronizar un filtro o una selección de pestaña en la URL donde añadir un paso al botón de retroceso sería molesto. Para eso sirve replaceState():
<div>
<button onclick="replaceCurrentState()">Replace State</button>
<p id="replace-status">Ready</p>
</div>
<script>
function replaceCurrentState() {
const newState = { id: 'replacedState' };
// Replace the current state
window.history.replaceState(newState, 'Replaced State', 'replaced-state-url');
document.getElementById('replace-status').textContent = 'State replaced successfully!';
}
</script>Esto actualiza la entrada actual en su lugar. El botón de retroceso sigue llevando a la página anterior, porque replaceState no empujó un nuevo paso.
Ejemplo completo estilo SPA
Ahora reunamos todo. El ejemplo siguiente simula una aplicación de una sola página: al hacer clic en un botón se intercambia el contenido de un div y se actualiza la URL con pushState, mientras que un listener de popstate restaura el contenido correcto cuando el usuario navega con los botones de retroceso/avance. Este es el mismo patrón que usa un enrutador del lado del cliente internamente.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>SPA Style History API Example</title>
</head>
<body>
<h1>Page Navigation with History API</h1>
<div id="content">Start Page</div>
<!-- Buttons for navigation -->
<button onclick="loadPage('page1')">Load Page 1</button>
<button onclick="loadPage('page2')">Load Page 2</button>
<button onclick="manualGoBack()">Go Back</button>
<button onclick="manualGoForward()">Go Forward</button>
<!-- Display the current status of the history -->
<p id="historyStatus">History Status: Start</p>
<script>
// Loads a "page" and updates the browser's history state
function loadPage(page) {
const state = { page: page }; // State to be pushed to history
history.pushState(state, `Page ${page}`, `${page}.html`); // Pushing state to the history
document.getElementById('content').innerHTML = `<h2>This is ${page.replace('page', 'Page ')}</h2>`; // Update the content
updateHistoryStatus(state); // Update the history status display
}
// Handles the browser's back and forward button actions
window.addEventListener('popstate', function(event) {
if (event.state) {
// Update the page content and history status when navigating through history
document.getElementById('content').innerHTML = `<h2>This is ${event.state.page.replace('page', 'Page ')}</h2>`;
updateHistoryStatus(event.state);
} else {
// Fallback content when the history does not have any state
document.getElementById('content').innerHTML = `<h2>Start Page</h2>`;
document.getElementById('historyStatus').textContent = "History Status: Start";
}
});
// Updates the display of the current history status
function updateHistoryStatus(state) {
document.getElementById('historyStatus').textContent = `History Status: ${state.page}`;
}
// Function to manually trigger going back in history
function manualGoBack() {
history.back();
}
// Function to manually trigger going forward in history
function manualGoForward() {
history.forward();
}
</script>
</body>
</html>Cómo funciona:
- Carga de páginas dinámica —
loadPage()cambia el contenido de un div y llama apushStatepara añadir una entrada al historial, de modo que cada "página" se convierte en un punto real de retroceso/avance. - Restauración en la navegación — el listener de
popstateleeevent.state.pagey vuelve a renderizar el contenido correspondiente; cuandoevent.stateesnullvuelve a la página de inicio. - UI familiar — los botones controlan
history.back()yhistory.forward(), dando una sensación de múltiples páginas sin ninguna recarga completa.
Errores comunes
Algunos comportamientos suelen sorprender a los desarrolladores:
pushStateno disparapopstate. Solo la navegación del usuario (retroceso/avance,history.go()) lo dispara. Después de unpushState, renderiza la nueva vista tú mismo en la misma función.- El argumento
titlees ignorado. Casi todos los navegadores lo ignoran, así que pasa"". Para cambiar el título de la pestaña, establecedocument.titledirectamente. - Las URLs deben tener el mismo origen. Pasar una
urlde origen cruzado lanza unSecurityError. La ruta se resuelve de forma relativa al documento actual. - El estado debe ser serializable. El object de estado se clona con el algoritmo de clonado estructurado, por lo que funciones, nodos DOM e instancias de clase no pueden almacenarse — y existe un límite de tamaño (comúnmente unos pocos MB).
- Una recarga de página vuelve a ejecutar tu aplicación en la nueva URL. Cuando el usuario actualiza una URL creada con
pushState, el servidor debe responder a esa ruta (o tu SPA debe manejarla al cargar), de lo contrario obtendrán un 404. Este fallback del lado del servidor es el problema de enrutamiento que las SPAs deben resolver.
Buenas prácticas para usar la History API
- Mantén el estado pequeño. Almacena un identificador o unos pocos valores primitivos en el object de estado y deriva el resto. No vuelques grandes conjuntos de datos en el historial — esto aumenta el tamaño de la sesión y puede alcanzar el límite de tamaño.
- Actualiza siempre
document.titletú mismo cuando cambie la "página", ya que el argumentotitlees ignorado. - Gestiona la posición del scroll. Los navegadores restauran el scroll en
popstatemediantehistory.scrollRestoration; establécelo en"manual"si quieres controlar el desplazamiento tú mismo. Consulta tamaños de ventana y desplazamiento para las APIs de scroll. - Proporciona un fallback del lado del servidor para que actualizar o compartir una URL creada con
pushStatesiga funcionando. - Usa
replaceStatepara actualizaciones no navegacionales (filtros, pestañas) para que el botón de retroceso siga siendo significativo. - Inspeccionar el historial — usa
history.statepara el object de estado actual yhistory.lengthpara el número de entradas en la pila de sesión.
Dado que la History API solo cambia la URL, se combina de forma natural con fetch para cargar los datos que cada "página" necesita.
Conclusión
La JavaScript History API te permite manipular el historial de sesión del navegador — añadiendo, reemplazando y reaccionando a la navegación — sin recargas completas de página. Dominar pushState, replaceState y el evento popstate, respetando los errores comunes relacionados con popstate, la serialización y las URLs de mismo origen, es lo que hace que las aplicaciones de una sola página se sientan tan rápidas y navegables como los sitios web tradicionales de múltiples páginas.