Islas Dinámicas con JS Vanilla
Aprende a crear zonas interactivas que se actualizan en tiempo real usando JavaScript puro, sin necesidad de React ni otros frameworks.
1. ¿Por qué necesitamos Islas Dinámicas?
El Problema con SSG Puro
Imagina este escenario: Tienes una tienda online con Astro SSG conectada a una API Spring Boot.
📅 Línea temporal del problema:
❌ Problema:
Los datos quedaron "congelados" en el momento del build.
Los 5 productos nuevos no aparecen hasta que hagas otro npm run build.
La Solución: Islas Dinámicas
Con Islas Dinámicas, ciertas partes de tu página se actualizan en el navegador del usuario, haciendo fetch a la API en tiempo real.
✅ Resultado:
El usuario siempre ve los datos actualizados, sin necesidad de rebuilds. Lo mejor de ambos mundos: página estática rápida + datos dinámicos frescos.
2. Arquitectura Visual
🏝️ Anatomía de una Página con Islas
• Se ejecuta en el NAVEGADOR
• Hace fetch() a la API
• Se actualiza en tiempo real
SSG Puro (Sin islas)
SSG + Islas Dinámicas
3. ❌ Lo que NO funciona (SSG puro)
Fetch en el Frontmatter
Se ejecuta en BUILD TIME, no en el navegador
1---2// ⚠️ ESTO SE EJECUTA SOLO EN BUILD TIME3// Si añades productos después del build, NO aparecerán4 5const response = await fetch('http://localhost:8080/api/productos')6const productos = await response.json()7 8// productos = [producto1, producto2, producto3]9// Estos datos quedan "congelados" en el HTML generado10---11 12<h1>Nuestros Productos</h1>13 14<ul>15 {productos.map(p => (16 <li>{p.nombre} - {p.precio}€</li>17 ))}18</ul>19 20<!--21 ❌ PROBLEMA: Si mañana añades producto4 en Spring Boot,22 este HTML seguirá mostrando solo 3 productos23 hasta que hagas otro "npm run build"24-->⚠️ Cuándo pasa esto:
- • POST /api/productos → Nuevo producto NO aparece
- • PUT /api/productos/1 → Cambio de precio NO se ve
- • DELETE /api/productos/1 → Producto eliminado SIGUE apareciendo
4. ✅ Lo que SÍ funciona (Isla Dinámica)
Fetch en un <script>
Se ejecuta en el NAVEGADOR del usuario
1---2// El frontmatter puede estar vacío o tener datos estáticos3// que NO necesitan actualizarse (título, metadata, etc.)4const pageTitle = "Nuestros Productos"5---6 7<h1>{pageTitle}</h1>8 9<!-- Contenedor vacío que se llenará con JavaScript -->10<div id="productos-container">11 <p class="loading">🔄 Cargando productos...</p>12</div>13 14<script>15 // ✅ ESTO SE EJECUTA EN EL NAVEGADOR16 // Cada vez que un usuario visita la página17 18 async function cargarProductos() {19 try {20 const response = await fetch('http://localhost:8080/api/productos')21 const productos = await response.json()22 23 const container = document.getElementById('productos-container')24 25 if (productos.length === 0) {26 container.innerHTML = '<p>No hay productos disponibles</p>'27 return28 }29 30 container.innerHTML = productos31 .map(p => `32 <div class="producto">33 <h3>${p.nombre}</h3>34 <p>${p.precio}€</p>35 </div>36 `)37 .join('')38 39 } catch (error) {40 document.getElementById('productos-container').innerHTML =41 '<p style="color: red;">Error al cargar productos</p>'42 }43 }44 45 // Cargar al iniciar46 cargarProductos()47 48 // Opcional: Actualizar cada 30 segundos49 setInterval(cargarProductos, 30000)50</script>✅ Ahora sí funciona:
- • POST /api/productos → Nuevo producto aparece inmediatamente
- • PUT /api/productos/1 → Cambio de precio se ve al refrescar
- • DELETE /api/productos/1 → Producto desaparece correctamente
5. Ejemplo Completo: Lista de Items
Este ejemplo combina datos estáticos (título de la página) con datos dinámicos (lista de items desde la API).
1---2// ⚠️ ESTO NO SE ACTUALIZA (se genera en build)3// Útil para configuración, metadata, textos fijos4const siteConfig = {5 title: "Mi Tienda Online",6 description: "Los mejores productos"7}8---9 10<html>11<head>12 <!-- Este título está "congelado" en el HTML -->13 <title>{siteConfig.title}</title>14 <meta name="description" content={siteConfig.description} />15</head>16<body>17 <header>18 <h1>{siteConfig.title}</h1>19 <!-- El header es estático, carga instantáneamente -->20 </header>21 22 <main>23 <!-- 🏝️ ISLA DINÁMICA: Esta sección se actualiza -->24 <section id="items-container">25 <p>🔄 Cargando items...</p>26 </section>27 </main>28 29 <footer>30 <!-- El footer es estático -->31 <p>© 2024 {siteConfig.title}</p>32 </footer>33 34 <script>35 // ✅ Este código se ejecuta en el NAVEGADOR36 37 async function loadItems() {38 const container = document.getElementById('items-container')39 40 try {41 // Indicador de carga (opcional)42 container.innerHTML = '<p>🔄 Actualizando...</p>'43 44 // Fetch a tu API Spring Boot45 const response = await fetch('http://localhost:8080/api/items')46 47 if (!response.ok) {48 throw new Error(`HTTP ${response.status}`)49 }50 51 const items = await response.json()52 53 // Renderizar los items54 container.innerHTML = items.length > 055 ? items.map(item => `56 <article class="item-card">57 <h3>${item.nombre}</h3>58 <p class="precio">${item.precio}€</p>59 <p class="stock">Stock: ${item.stock}</p>60 </article>61 `).join('')62 : '<p>No hay items disponibles</p>'63 64 } catch (error) {65 console.error('Error:', error)66 container.innerHTML = `67 <div class="error">68 <p>❌ Error al cargar los datos</p>69 <button onclick="loadItems()">Reintentar</button>70 </div>71 `72 }73 }74 75 // Carga inicial cuando la página está lista76 document.addEventListener('DOMContentLoaded', loadItems)77 78 // Actualización automática cada 10 segundos79 setInterval(loadItems, 10000)80 </script>81 82 <style>83 .item-card {84 border: 1px solid #ccc;85 padding: 1rem;86 margin: 0.5rem 0;87 border-radius: 8px;88 }89 .precio { color: green; font-weight: bold; }90 .stock { color: gray; font-size: 0.9rem; }91 .error { color: red; text-align: center; }92 .error button { margin-top: 1rem; }93 </style>94</body>95</html>6. ¿Cuándo usar cada enfoque?
| Tipo de dato | SSG (Frontmatter) | Isla (Script) |
|---|---|---|
| Título de la página | ✅ Ideal | No necesario |
| Metadata SEO | ✅ Ideal | No funciona para SEO |
| Menú de navegación | ✅ Ideal | No necesario |
| Lista de productos (CRUD) | ❌ Se congela | ✅ Siempre actualizado |
| Carrito de compras | ❌ Imposible | ✅ Necesario |
| Stock en tiempo real | ❌ Desactualizado | ✅ Con polling |
| Comentarios / Reviews | ❌ Se congela | ✅ Siempre actualizado |
💡 Regla general: Si los datos pueden cambiar después del build (por operaciones CRUD en tu API), usa una Isla Dinámica. Si son datos fijos (textos, configuración), usa el Frontmatter.
7. Resumen Visual
Frontmatter (---)
---
const data = await fetch(...)
---
- Se ejecuta en el servidor
- Solo en build time
- Ideal para SEO
Script (<script>)
<script>
fetch(...).then(...)
</script>
- Se ejecuta en el navegador
- En cada visita/refresh
- Ideal para datos dinámicos