Creando una Single Page Application (SPA) desde cero con React
Una Single Page Application (SPA) es una aplicación web que no recarga la página al navegar entre diferentes rutas. En lugar de solicitar nuevas páginas al servidor, la SPA carga todo el código necesario al inicio y luego renderiza contenido dinámicamente según la URL.
En esta clase aprenderás a crear tu propia SPA sin usar React Router ni otras librerías, implementando todo el sistema de navegación desde cero. Esto te ayudará a entender cómo funcionan las SPAs por dentro antes de usar herramientas más avanzadas.
¿Por qué crear una SPA?
Navegación tradicional (Multi-Page Application)
En una web tradicional, cada vez que haces clic en un enlace:
1. Se hace una petición al servidor
2. El servidor devuelve un HTML completo
3. El navegador descarga de nuevo CSS, JavaScript, imágenes, fuentes...
4. Se pierde el estado de la aplicación
5. Hay un "parpadeo" mientras carga la nueva página
Navegación SPA
En una SPA, cuando haces clic:
1. Se previene la recarga de la página
2. Se cambia la URL del navegador
3. React re-renderiza el contenido correspondiente
4. NO se vuelven a descargar recursos
5. Navegación instantánea y fluida
Ventajas:
- ⚡ Navegación instantánea: no se recargan recursos
- 💾 Mantiene el estado: las variables y datos persisten
- 🎨 Mejor UX: transiciones suaves, sin parpadeos
- 📦 Menos datos: solo se descarga contenido nuevo
El problema: Los enlaces recargan la página
Por defecto, cuando haces clic en un <a>:
function Header() {
return (
<header>
<a href="/">Inicio</a>
<a href="/about">Acerca de</a>
<a href="/contact">Contacto</a>
</header>
)
}
Cada clic recarga toda la página → descarga de nuevo HTML, CSS, JS, imágenes…
Solución: Crear un componente Link personalizado
Vamos a crear nuestro propio componente <Link> que intercepta los clics y evita la recarga:
function Link({ href, children, ...props }) {
const handleClick = (event) => {
// 1. Prevenir el comportamiento por defecto (no recargar)
event.preventDefault()
// 2. Cambiar la URL sin recargar la página
window.history.pushState({}, '', href)
// 3. Emitir un evento para notificar el cambio
const navigationEvent = new PopStateEvent('popstate')
window.dispatchEvent(navigationEvent)
}
return (
<a href={href} onClick={handleClick} {...props}>
{children}
</a>
)
}
¿Qué hace cada parte?
1. preventDefault()
Evita que el navegador haga el comportamiento por defecto, que es navegar al enlace.
event.preventDefault()
Sin esto, el navegador seguiría el enlace de forma tradicional.
2. pushState()
Cambia la URL del navegador sin recargar la página:
window.history.pushState({}, '', href)
- Primer parámetro: objeto de estado (lo dejamos vacío)
- Segundo parámetro: título (obsoleto, lo dejamos vacío)
- Tercer parámetro: la nueva URL
Después de esto, la barra de direcciones muestra la nueva URL, pero la página no se recarga.
3. Emitir evento popstate
Para que React se entere del cambio de URL, emitimos manualmente un evento popstate:
const navigationEvent = new PopStateEvent('popstate')
window.dispatchEvent(navigationEvent)
Este evento se dispara automáticamente cuando el usuario usa los botones de atrás/adelante del navegador, pero como nosotros estamos cambiando la URL manualmente con pushState, debemos emitirlo nosotros.
Reemplazar los enlaces tradicionales
Ahora sustituimos todos los <a> por nuestro componente <Link>:
function Header() {
return (
<header>
<Link href="/">Inicio</Link>
<Link href="/about">Acerca de</Link>
<Link href="/contact">Contacto</Link>
</header>
)
}
¡Genial! Ahora al hacer clic:
- ✅ La URL cambia
- ✅ No se recarga la página
- ❌ Pero… el contenido no cambia
El problema: React no reacciona al cambio de URL
Aunque la URL cambia, ningún componente se está re-renderizando porque React no sabe que la URL ha cambiado.
Necesitamos:
- Escuchar cambios en la URL
- Actualizar un estado de React con la nueva ruta
- Renderizar el contenido correspondiente según esa ruta
Solución: Mantener la ruta en el estado
Vamos a crear un estado que contenga la ruta actual:
import { useState, useEffect } from 'react'
function App() {
const [currentPath, setCurrentPath] = useState(window.location.pathname)
return (
<div>
<Header />
<main>
<h1>Ruta actual: {currentPath}</h1>
</main>
</div>
)
}
Ahora tenemos la ruta en el estado, pero todavía no se actualiza cuando navegamos.
Escuchar cambios de URL con useEffect
Necesitamos escuchar el evento popstate para detectar cuando la URL cambia:
import { useState, useEffect } from 'react'
function App() {
const [currentPath, setCurrentPath] = useState(window.location.pathname)
useEffect(() => {
// Función que actualiza el estado con la nueva ruta
const handleLocationChange = () => {
setCurrentPath(window.location.pathname)
}
// Escuchar el evento popstate
window.addEventListener('popstate', handleLocationChange)
// Limpiar el event listener al desmontar
return () => {
window.removeEventListener('popstate', handleLocationChange)
}
}, [])
return (
<div>
<Header />
<main>
<h1>Ruta actual: {currentPath}</h1>
</main>
</div>
)
}
Importante: Usamos la misma referencia de función (handleLocationChange) para añadir y eliminar el listener. Si usáramos funciones anónimas diferentes, no se limpiaría correctamente.
¿Por qué el array vacío []?
useEffect(() => {
// ...
}, [])
Queremos que el listener se añada solo una vez cuando el componente se monta, no en cada render. Por eso usamos un array vacío como dependencia.
Renderizar contenido según la ruta
Ahora que tenemos currentPath actualizado, podemos renderizar contenido diferente según la ruta:
function App() {
const [currentPath, setCurrentPath] = useState(window.location.pathname)
useEffect(() => {
const handleLocationChange = () => {
setCurrentPath(window.location.pathname)
}
window.addEventListener('popstate', handleLocationChange)
return () => {
window.removeEventListener('popstate', handleLocationChange)
}
}, [])
return (
<div>
<Header />
<main>
{currentPath === '/' && <HomePage />}
{currentPath === '/search' && <SearchPage />}
</main>
</div>
)
}
¡Ya tenemos una SPA funcional!
El flujo completo
Cuando el usuario hace clic en un <Link>:
1. Link captura el clic → event.preventDefault()
2. Cambia la URL → window.history.pushState({}, '', href)
3. Emite el evento → window.dispatchEvent(new PopStateEvent('popstate'))
4. useEffect detecta el evento → handleLocationChange()
5. Actualiza el estado → setCurrentPath(window.location.pathname)
6. React re-renderiza → muestra el componente correspondiente
¡Ya es una SPA! Comprobación con DevTools
Abre las DevTools del navegador → pestaña Network (Red):
Navegación tradicional (sin SPA)
Al hacer clic en un enlace <a> normal:
✅ index.html 200 OK 1.2 KB
✅ styles.css 200 OK 450 B
✅ bundle.js 200 OK 234 KB
✅ logo.png 200 OK 45 KB
✅ font.woff2 200 OK 78 KB
---------------------------------
Total: 357 KB descargados
Navegación SPA (con nuestro Link)
Al hacer clic en nuestro <Link>:
(vacío, no se descarga nada)
¡Navegación instantánea! No se recargan recursos porque la página nunca se recarga.
Comparación con red lenta
Para ver la diferencia más claramente, simula una conexión lenta:
- Abre DevTools → Network
- Cambia “No throttling” a “Slow 3G”
- Haz clic en un enlace tradicional → tarda varios segundos
- Haz clic en un
<Link>de tu SPA → instantáneo
Esta es la gran ventaja de las SPAs: experiencia de usuario mucho más rápida y fluida.
Soportar botones Atrás/Adelante del navegador
Nuestra SPA ya soporta los botones de navegación del navegador sin código adicional porque:
- Cuando el usuario hace clic en “Atrás” o “Adelante”
- El navegador emite automáticamente el evento
popstate - Nuestro
useEffectlo detecta y actualiza el estado - React re-renderiza el contenido correspondiente
¡Todo funciona! 🎉
¿Y React Router?
Ahora que entiendes cómo funciona una SPA por dentro, en las próximas clases usaremos React Router, que es una librería profesional que:
- ✅ Maneja rutas dinámicas (
/user/:id) - ✅ Permite rutas anidadas
- ✅ Facilita la navegación programática
- ✅ Soporta lazy loading de componentes
- ✅ Incluye hooks como
useNavigate,useParams, etc.
Pero es importante entender qué hace React Router por dentro antes de usarlo. ¡Ahora ya lo sabes!
Resumen
En esta clase aprendiste a crear una SPA desde cero:
- 🔗 Crear un componente Link que intercepta clics
- 🚫 Usar preventDefault() para evitar recargas
- 🌐 Cambiar la URL con
window.history.pushState - 📢 Emitir eventos popstate para notificar cambios
- 👂 Escuchar cambios con
useEffecty event listeners - 🔄 Mantener la ruta en estado para re-renderizar componentes
- ⚡ Navegación instantánea sin descargar recursos de nuevo
Ventajas de las SPAs
- ⚡ Rendimiento: navegación instantánea
- 💾 Estado persistente: no se pierde información al navegar
- 🎨 Mejores transiciones: animaciones fluidas entre páginas
- 📦 Menos datos: solo se descarga contenido nuevo
- 🚀 Experiencia de usuario: se siente como una app nativa
Recuerda: React Router hace todo esto (y más) por ti, pero ahora entiendes cómo funciona por dentro. ¡En la próxima clase veremos React Router!
💡 Tip: Abre las DevTools → Network y compara la navegación tradicional vs SPA. Verás que con SPA no se descarga nada al navegar.