astro-estado-compartido-nanostores

Compartir estado entre Islas con Nano Stores

  • 5 min

El modelo de “Islas” de Astro es fantástico para el rendimiento, pero nos plantea un problemita de arquitectura importante.

Si tengo un Botón de Compra en React en la cabecera y un Carrito en Svelte en la barra lateral… ¿cómo hago para que al pulsar el botón se actualice el carrito?

En una aplicación tradicional (Next.js, CRA), envolveríamos todo en un CartContext. En Astro no podemos hacer esto, porque cada isla se hidrata de forma independiente. No comparten un ancestro común en el DOM virtual.

La solución es utilizar una librería de gestión de estado que sea agnóstica del framework. La recomendación oficial de Astro es Nano Stores.

¿Qué es Nano Stores?

Nano Stores es una librería de gestión de estado minúscula (menos de 1KB) diseñada para moverse entre frameworks facilmente.

Ventajas de Nano Stores

  1. Agnóstica: Funciona con React, Vue, Svelte, Solid, Vanilla JS y Astro.
  2. Ligera: No añade peso innecesario a tu bundle.
  3. Sin dependencias: No requiere configuraciones complejas.

Instalación

Para usarla, necesitamos instalar el núcleo y, opcionalmente, los adaptadores para los frameworks que estemos usando.

npm install nanostores @nanostores/react @nanostores/vue @nanostores/svelte

Como usar Nano Stores con Astro

Vamos a ver como usar Nano Stores con Astro con un ejemplo donde vamos a emitir un evento desde un componente de React y recibirlo en un componente de Svelte.

Paso 1: Crear el Store

Lo primero es definir nuestro estado. A diferencia de Redux, no necesitamos un archivo central gigante. Podemos crear pequeños archivos .ts o .js donde lo necesitemos.

Vamos a crear un archivo para gestionar un carrito de la compra simple.

// src/stores/cartStore.ts
import { atom, map } from 'nanostores';

// 'atom' se usa para valores simples (strings, números, booleanos)
export const isCartOpen = atom(false);

// 'map' se usa para objetos o diccionarios
export const cartItems = map<Record<string, number>>({});

/**
 * Función auxiliar para añadir items
 * Es buena práctica definir la lógica de modificación aquí, 
 * fuera de los componentes UI.
 */
export function addCartItem(id: string) {
  const existingEntry = cartItems.get()[id];
  if (existingEntry) {
    cartItems.setKey(id, existingEntry + 1);
  } else {
    cartItems.setKey(id, 1);
  }
}
Copied!

Fíjate que este archivo es TypeScript/JavaScript puro. No importa React, ni Vue, ni nada. Esto permite que cualquier parte de tu aplicación (incluso scripts inline) pueda leerlo o escribir en él.

Paso 2: El emisor (React)

Vamos a crear un componente en React que añade productos al carrito. Este componente escribirá en la store.

// src/components/AddToCartButton.tsx
import { useStore } from '@nanostores/react';
import { addCartItem, isCartOpen } from '../stores/cartStore';

export default function AddToCartButton({ productId }: { productId: string }) {
  // Leemos el estado (opcional, solo si necesitamos reaccionar a cambios)
  const $isOpen = useStore(isCartOpen);

  return (
    <button
      className="bg-blue-600 text-white px-4 py-2 rounded"
      onClick={() => {
        // 1. Ejecutamos la lógica de negocio
        addCartItem(productId);
        
        // 2. Modificamos el estado del átomo directamente
        isCartOpen.set(true); 
      }}
    >
      Añadir al Carrito (El panel está {$isOpen ? 'Abierto' : 'Cerrado'})
    </button>
  );
}
Copied!
  • Para leer el estado en React usamos el hook useStore.
  • Para escribir el estado, usamos el método .set() del átomo o llamamos a nuestra función auxiliar.

Paso 3: El receptor (Svelte)

Ahora vamos a crear la visualización del carrito usando Svelte. Svelte tiene una integración nativa genial con el patrón de observables, por lo que usar Nano Stores es increíblemente limpio.

<script>
  // En Svelte importamos la store directamente
  import { isCartOpen, cartItems } from '../stores/cartStore';
</script>

{#if $isCartOpen}
  <div class="cart-panel">
    <h3>Tu Carrito</h3>
    <ul>
      {#each Object.entries($cartItems) as [id, qty]}
        <li>Producto {id}: <strong>{qty}</strong></li>
      {/each}
    </ul>
    
    <button on:click={() => isCartOpen.set(false)}>Cerrar</button>
  </div>
{/if}

<style>
  .cart-panel {
    border: 1px solid #ccc;
    padding: 1rem;
    background: #f9f9f9;
    position: fixed;
    top: 20px;
    right: 20px;
  }
</style>
Copied!

¿Ves lo limpio que es Svelte? Simplemente poniendo $isCartOpen, Svelte se encarga de todo.

Si vienes de React, esto te parecerá brujeria, pero es estándar en Svelte 😉.

Paso 4: Uniendo todo en Astro

Finalmente, juntamos nuestras islas en una página.

---
// src/pages/tienda.astro
import Layout from '../layouts/Layout.astro';
import AddToCartButton from '../components/AddToCartButton';
import CartFlyout from '../components/CartFlyout.svelte';
---

<Layout title="Tienda">
  <h1>Ejemplo de Estado Compartido</h1>

  <div class="grid-productos">
    <AddToCartButton client:visible productId="prod-1" />
    <AddToCartButton client:visible productId="prod-2" />
  </div>

  <CartFlyout client:idle />

</Layout>
Copied!

Cuando hagas clic en el botón renderizado por React:

  1. React ejecutará addCartItem.
  2. Nano Store actualizará el valor en memoria y emitirá un evento de cambio.
  3. El componente de Svelte (que está suscrito) recibirá el aviso y se re-renderizará instantáneamente.

Uso con Vanilla JS

A veces no necesitas un framework de UI. A veces solo quieres un <script> vanilla JS dentro de tu archivo .astro que también reaccione.

<script>
  import { isCartOpen } from '../stores/cartStore';

  // Suscribirse a cambios en vanilla JS
  isCartOpen.subscribe(open => {
    console.log('El estado del carrito ha cambiado a:', open);
    if(open) {
      document.body.classList.add('carrito-abierto');
    } else {
      document.body.classList.remove('carrito-abierto');
    }
  });
</script>
Copied!

Como vemos también es posible y muy sencillo hacerlo