Volver al inicio
Lección 9: Islas Dinámicas con JS Vanilla

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:

Lunes 10:00
npm run build → Se genera HTML con 50 productos
Lunes 14:00
POST /api/productos → Añades 5 productos nuevos en Spring
Lunes 15:00
Usuario visita la web → ¡Solo ve 50 productos! 💀☠️☠️☠️☠️💀

❌ 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

PÁGINA ASTRO (SSG)
<Header> HTML estático
🏝️ ISLA DINÁMICA
<div id="productos">

• Se ejecuta en el NAVEGADOR

• Hace fetch() a la API

• Se actualiza en tiempo real

</div>
<section> Sobre nosotros... </section> HTML estático
<Footer> HTML estático
fetch() cada X segundos
API Spring Boot localhost:8080/api/productos

SSG Puro (Sin islas)

Datos "congelados" en build time
POST/PUT/DELETE no se reflejan
Necesitas rebuild para actualizar

SSG + Islas Dinámicas

Página base ultra-rápida (estática)
Datos siempre actualizados (fetch)
Sin rebuilds necesarios

3. ❌ Lo que NO funciona (SSG puro)

Fetch en el Frontmatter

Se ejecuta en BUILD TIME, no en el navegador

productos.astro - ❌ SSG Puro (datos congelados)astro
1---
2// ⚠️ ESTO SE EJECUTA SOLO EN BUILD TIME
3// Si añades productos después del build, NO aparecerán
4
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 generado
10---
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 productos
23 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

productos.astro - ✅ Con Isla Dinámicaastro
1---
2// El frontmatter puede estar vacío o tener datos estáticos
3// 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 NAVEGADOR
16 // Cada vez que un usuario visita la página
17
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 return
28 }
29
30 container.innerHTML = productos
31 .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 iniciar
46 cargarProductos()
47
48 // Opcional: Actualizar cada 30 segundos
49 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).

tienda.astro - Ejemplo completo con isla dinámicaastro
1---
2// ⚠️ ESTO NO SE ACTUALIZA (se genera en build)
3// Útil para configuración, metadata, textos fijos
4const 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 NAVEGADOR
36
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 Boot
45 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 items
54 container.innerHTML = items.length > 0
55 ? 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á lista
76 document.addEventListener('DOMContentLoaded', loadItems)
77
78 // Actualización automática cada 10 segundos
79 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

Conclusión

Las Islas Dinámicas permiten tener datos actualizados sin perder las ventajas del SSG
Usa el frontmatter para datos estáticos (SEO, configuración, textos fijos)
Usa <script> con fetch() para datos que cambian (CRUD, tiempo real)
No necesitas React ni otros frameworks - JavaScript vanilla es suficiente
Combina lo mejor de ambos mundos: velocidad estática + datos frescos