Astro กับ Nanostores: Shared State ระหว่าง Islands

#astro13 เม.ย. 2569

ปัญหา State ใน Islands Architecture

เมื่อใช้ Islands Architecture แต่ละ island เป็น independent component ทำให้การแชร์ state ระหว่าง islands ทำได้ยาก Nanostores แก้ปัญหานี้ได้

ติดตั้ง

npm install nanostores @nanostores/react
# หรือ
npm install nanostores @nanostores/vue

สร้าง Store

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

// Simple atom
export const cartOpen = atom(false);

// Map (object)
export const cartItems = map<Record<string, { name: string; price: number; qty: number }>>({});

// Computed
export const cartTotal = computed(cartItems, items =>
  Object.values(items).reduce((sum, item) => sum + item.price * item.qty, 0)
);

export const cartCount = computed(cartItems, items =>
  Object.values(items).reduce((sum, item) => sum + item.qty, 0)
);

// Actions
export function addToCart(id: string, name: string, price: number) {
  const current = cartItems.get()[id];
  if (current) {
    cartItems.setKey(id, { ...current, qty: current.qty + 1 });
  } else {
    cartItems.setKey(id, { name, price, qty: 1 });
  }
}

export function removeFromCart(id: string) {
  const items = { ...cartItems.get() };
  delete items[id];
  cartItems.set(items);
}

ใช้ใน React Island

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

export default function CartButton() {
  const count = useStore(cartCount);
  const open = useStore(cartOpen);

  return (
    <button onClick={() => cartOpen.set(!open)}>
      🛒 ({count})
    </button>
  );
}

ใช้ใน Vue Island

<!-- src/components/CartDrawer.vue -->
<script setup>
import { useStore } from '@nanostores/vue';
import { cartItems, cartTotal, removeFromCart } from '../stores/cart';

const items = useStore(cartItems);
const total = useStore(cartTotal);
</script>

<template>
  <div v-for="(item, id) in items" :key="id">
    {{ item.name }} x{{ item.qty }} = ฿{{ item.price * item.qty }}
    <button @click="removeFromCart(id)">ลบ</button>
  </div>
  <p>รวม: ฿{{ total }}</p>
</template>

ใช้ใน Astro Page

---
import CartButton from '../components/CartButton.tsx';
import CartDrawer from '../components/CartDrawer.vue';
import ProductCard from '../components/ProductCard.tsx';
---

<CartButton client:load />
<CartDrawer client:load />
<ProductCard client:visible />
<!-- ทั้งสาม island แชร์ cart state เดียวกัน -->

สรุป

Nanostores เป็น state management ที่เบามาก (< 1KB) และทำงานได้กับทุก framework ทำให้แชร์ state ระหว่าง React, Vue, Svelte islands ได้อย่างง่ายดายครับ