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?

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

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…

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:

  1. Escuchar cambios en la URL
  2. Actualizar un estado de React con la nueva ruta
  3. 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):

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

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:

  1. Abre DevToolsNetwork
  2. Cambia “No throttling” a “Slow 3G”
  3. Haz clic en un enlace tradicional → tarda varios segundos
  4. 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:

  1. Cuando el usuario hace clic en “Atrás” o “Adelante”
  2. El navegador emite automáticamente el evento popstate
  3. Nuestro useEffect lo detecta y actualiza el estado
  4. 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 useEffect y 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.