ข้ามไปยังเนื้อหา

แชร์ State ข้าม Islands

island hydrate อย่างเป็นอิสระ — นั่นแหละที่ทำให้ต้นทุนถูก แต่มีผลข้างเคียงที่คนมาจาก SPA มักสะดุด: island สองตัวไม่แชร์ state กันอัตโนมัติ useState ของ React ใน island หนึ่งมองไม่เห็นจากอีก island หนึ่ง แม้จะสร้างจาก framework เดียวกันก็ตาม ไม่มี app tree ร่วมกัน แต่ละ island เป็น root เล็ก ๆ ของตัวเอง

แล้ว island “add to cart” กับ island “cart count” ที่ header จะ sync กันได้ยังไง? คุณมีเครื่องมือสามอย่าง เรียงจากง่ายที่สุดไปทรงพลังที่สุด

ถ้าข้อมูลรู้อยู่แล้วตอนหน้า render คำตอบที่ง่ายที่สุดคือคำนวณครั้งเดียวในไฟล์ .astro แล้วส่งให้แต่ละ island เป็น prop ไม่ต้องประสานงานข้าม island เลย — server คือแหล่งข้อมูลร่วม

---
import CartBadge from '../components/CartBadge.jsx';
const initialCount = await getCartCount(Astro.request);
---
<CartBadge count={initialCount} client:load />

อันนี้ครอบคลุม state เริ่มต้น แต่ไม่ช่วยตอนที่ island หนึ่งต้อง อัปเดต อีกตัวตอน runtime — นั่นคือสิ่งที่ store มีไว้

เมื่อ island ต้องตอบสนองต่อกันตอน runtime ให้ใช้ shared store ที่อยู่นอก island ตัวใดตัวหนึ่ง เอกสารของ Astro แนะนำ nanostores — ขนาดเล็กมาก ไม่ผูกกับ framework ใด และมี adapter สำหรับแต่ละ framework (@nanostores/react, @nanostores/vue, …) คุณนิยาม store ครั้งเดียว แล้ว island ไหนก็อ่านและเขียน store นี้ได้

src/stores/cart.js
import { atom } from 'nanostores';
export const cartCount = atom(0);
// AddToCart.jsx (React island)
import { cartCount } from '../stores/cart.js';
export default function AddToCart() {
return <button onClick={() => cartCount.set(cartCount.get() + 1)}>Add</button>;
}
// CartBadge.jsx (another React island, elsewhere on the page)
import { useStore } from '@nanostores/react';
import { cartCount } from '../stores/cart.js';
export default function CartBadge() {
const count = useStore(cartCount); // re-renders when the store changes
return <span>Cart: {count}</span>;
}

ตอนนี้กดปุ่ม “Add” ใน island หนึ่งจะอัปเดต badge ใน island ที่แยกกันคนละที่ — เพราะทั้งคู่ subscribe store เดียวกัน ไม่ได้ subscribe กันเอง

flowchart TB
  store["nanostore: cartCount"]
  add["AddToCart island — writes"] --> store
  store --> badge["CartBadge island — reads and re-renders"]
island สองตัวที่แยกกัน แชร์ nanostore เดียวกัน

บางครั้งคุณไม่ได้ต้องแชร์ state — แต่ต้องเอา content ที่ render บน server ไปวางในเปลือกที่ interactive framework island รับ <slot /> ได้แบบเดียวกับ .astro component ดังนั้น island <Tabs> ที่ interactive จึงห่อ panel ที่ static และ render บน server ไว้ได้ เปลือกเป็น interactive ส่วน content ถูกส่งเป็น HTML โดยไม่มี island JS เพิ่ม

---
import Accordion from '../components/Accordion.jsx';
---
<Accordion client:visible>
<p>This paragraph is server-rendered HTML, passed into the island via its slot.</p>
</Accordion>
  • ข้อมูลรู้ตอน render ไม่มีอัปเดต runtime → props จาก server
  • island ต้องอัปเดตกันตอน runtime → nanostore
  • ต้องการ chrome ที่ interactive ครอบ content ที่ static → slot

คว้า store มาใช้เฉพาะตอนที่มีการประสานงาน runtime ระหว่าง island แยก ๆ จริง ๆ ความต้องการ “shared state” ส่วนใหญ่กลายเป็น props เริ่มต้นหรือ slot ซึ่งส่ง JavaScript น้อยกว่า

ทำไม useState ใน island หนึ่งถึงไม่ปรากฏในอีก island?
วิธีที่แนะนำในการแชร์ state ที่ต้องอัปเดตตอน runtime ข้าม island แยก ๆ คืออะไร?
คุณต้องการ cart count เริ่มต้นใน island สองตัว โดยไม่มีอัปเดต runtime ระหว่างกัน วิธีที่ง่ายที่สุด?
การส่ง children ผ่าน slot เข้าไปใน framework island ทำให้ทำอะไรได้?