Aprende a crear tu propia SPA sin dependencias. Implementa navegación interna, intercepta clics, manipula el historial del navegador y renderiza contenido según la ruta usando solo React y JavaScript nativo
midudev
Más clases y directos en YouTube
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 servidor2. El servidor devuelve un HTML completo3. El navegador descarga de nuevo CSS, JavaScript, imágenes, fuentes...4. Se pierde el estado de la aplicación5. 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ágina2. Se cambia la URL del navegador3. React re-renderiza el contenido correspondiente4. NO se vuelven a descargar recursos5. 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.
