Ilustración de Nanostores State Sharing
Ilustración de Nanostores State Sharing

Compartiendo Estado Entre Frameworks con Nanostores

En Islands Architecture podés tener un componente en React, otro en Vue y otro en Svelte en la misma página. El problema es que cada framework tiene su propio sistema de estado — Redux no le habla a Svelte stores, y el Context de React no cruza límites de framework.

La solución que uso en este sitio es Nanostores. Son ~300 bytes, sin dependencias, y funcionan igual en React, Vue y Svelte.

El store compartido

// src/stores/cart.ts
import { atom, computed } from 'nanostores'

export interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
}

// Atom: valor reactivo simple
export const cartItems = atom<CartItem[]>([])

// Computed: estado derivado
export const cartTotal = computed(cartItems, items =>
  items.reduce((sum, item) => sum + item.price * item.quantity, 0)
)

export const cartCount = computed(cartItems, items =>
  items.reduce((sum, item) => sum + item.quantity, 0)
)

// Actions: mutaciones de estado
export function addToCart(item: Omit<CartItem, 'quantity'>) {
  const items = cartItems.get()
  const existing = items.find(i => i.id === item.id)

  if (existing) {
    cartItems.set(
      items.map(i =>
        i.id === item.id
          ? { ...i, quantity: i.quantity + 1 }
          : i
      )
    )
  } else {
    cartItems.set([...items, { ...item, quantity: 1 }])
  }
}

export function removeFromCart(id: string) {
  cartItems.set(cartItems.get().filter(item => item.id !== id))
}

En React

// components/react/CartButton.tsx
import { useStore } from '@nanostores/react'
import { cartCount, cartItems } from '@/stores/cart'

export function CartButton() {
  const count = useStore(cartCount)
  const items = useStore(cartItems)

  return (
    <button className="cart-button">
      Carrito ({count})
      {items.length > 0 && (
        <span className="badge">{items.length} artículos</span>
      )}
    </button>
  )
}

En Svelte

<!-- components/svelte/CartTotal.svelte -->
<script>
import { cartTotal, cartItems } from '@/stores/cart'

// Nanostores funcionan directamente con la sintaxis $ de Svelte
$: total = $cartTotal
$: itemCount = $cartItems.length
</script>

<div class="cart-total">
  <p>{itemCount} artículos en el carrito</p>
  <p class="total">Total: ${total.toFixed(2)}</p>
</div>

Fijate que en Svelte no hace falta ningún helper extra — la sintaxis $store funciona directo con Nanostores.

Juntándolo en Astro

---
// pages/shop.astro
import CartButton from '@/components/react/CartButton'
import ProductCard from '@/components/vue/ProductCard.vue'
import CartTotal from '@/components/svelte/CartTotal.svelte'

const products = [
  { id: '1', name: 'Widget', price: 29.99 },
  { id: '2', name: 'Gadget', price: 49.99 },
]
---

<header>
  <CartButton client:load />
</header>

<main>
  <div class="products">
    {products.map(product => (
      <ProductCard
        client:visible
        id={product.id}
        name={product.name}
        price={product.price}
      />
    ))}
  </div>

  <aside>
    <CartTotal client:idle />
  </aside>
</main>

El botón React, las tarjetas Vue y el total Svelte comparten el mismo estado. Cuando alguien hace click en “Agregar al carrito” en una tarjeta Vue, el botón React y el total Svelte se actualizan solos.

Ojo con instalar los helpers por framework:

npm install nanostores
npm install @nanostores/react   # Para React
npm install @nanostores/vue     # Para Vue
# Svelte funciona directo, sin paquete extra

Para Islands Architecture, Nanostores es la solución más simple que encontré. Eso es todo por ahora.

¡Link copiado!

Comments for shrngs