Berbagi state antar island
Tiap island adalah aplikasi terpisah. Untuk yang benar-benar harus berkomunikasi ada nanostores; tapi tanyakan dulu apakah state itu memang perlu dibagi.
Intisari
- Island tidak berbagi
provide/inject, Pinia, maupun instance apa pun. - Nanostores hidup di luar framework, jadi satu store bisa dibaca island Vue mana pun โ ukurannya di bawah 1 KB.
- State di MPA hilang saat navigasi. Yang harus bertahan disimpan di
localStorageatau URL. - Untuk banyak kasus, jawaban yang benar adalah menggabungkan dua island jadi satu.
- State yang harus benar di seluruh perangkat โ status langganan, bookmark โ milik server, bukan klien.
Kenapa ini jadi masalah
<TombolBookmark client:load artikelId={artikel.id} />
<!-- โฆ 800 piksel ke bawah โฆ -->
<PanelBookmark client:visible />
Pembaca menekan tombol bookmark. Panel di bawah harus ikut berubah. Tapi keduanya aplikasi Vue terpisah โ tidak ada parent bersama, tidak ada store bersama, tidak ada cara memanggil satu dari yang lain.
Nanostores
pnpm add nanostores @nanostores/vue
// src/stores/bookmark.ts โ bukan berkas Vue, sengaja
import { atom, computed } from "nanostores";
export const bookmark = atom<number[]>([]);
export const jumlahBookmark = computed(bookmark, (b) => b.length);
export function toggle(id: number) {
const kini = bookmark.get();
bookmark.set(kini.includes(id) ? kini.filter((x) => x !== id) : [...kini, id]);
}
// src/components/TombolBookmark.vue
<script setup lang="ts">
import { useStore } from "@nanostores/vue";
import { bookmark, toggle } from "@/stores/bookmark";
import { computed } from "vue";
const props = defineProps<{ artikelId: number }>();
const daftar = useStore(bookmark);
const aktif = computed(() => daftar.value.includes(props.artikelId));
</script>
<template>
<button @click="toggle(artikelId)" :aria-pressed="aktif">
{{ aktif ? "โ
Tersimpan" : "โ Simpan" }}
</button>
</template>
Store-nya berupa modul JavaScript biasa. Karena kedua island mengimpor modul yang sama, keduanya mendapat
instance yang sama โ dan perubahan di satu terlihat di yang lain. Tidak ada plugin, tidak ada
app.use(), tidak ada provider yang harus membungkus keduanya.
Bertahan lintas navigasi
Astro adalah MPA. Tiap klik tautan memuat dokumen baru, dan seluruh state JavaScript hilang. Untuk state yang harus bertahan:
import { persistentAtom } from "@nanostores/persistent";
export const bookmark = persistentAtom<number[]>("bookmark", [], {
encode: JSON.stringify,
decode: (s) => {
try {
const v = JSON.parse(s);
return Array.isArray(v) ? v.filter((x) => typeof x === "number") : [];
} catch {
return [];
}
},
});
decode yang defensif itu bukan berlebihan. Isi localStorage bisa
disunting siapa pun lewat DevTools, dan bisa berisi format dari versi aplikasimu yang lama.
JSON.parse yang melempar di dalam inisialisasi store akan menggagalkan seluruh island โ
dan pembaca yang terkena tidak punya cara memperbaikinya selain membersihkan data situs.
Tanyakan dulu: apakah harus dibagi?
| Situasi | Jawaban |
|---|---|
| Dua island bersebelahan di DOM | Gabungkan jadi satu island. Paling sederhana, paling cepat |
| State-nya cuma UI: tab aktif, panel terbuka | Simpan lokal di komponennya |
| State-nya layak dibagikan lewat tautan | URL, bukan store |
| Preferensi per perangkat: tema, ukuran font | persistentAtom |
| Berlaku di semua perangkat: bookmark, langganan | Server. localStorage tidak ikut pindah perangkat |
| Island berjauhan tapi harus sinkron dalam satu halaman | Nanostores |
Baris kelima itu keputusan produk yang sering salah diambil sebagai keputusan teknis. Bookmark
di localStorage terasa cukup sampai pembaca membuka portal di ponselnya dan menemukan
daftarnya kosong. Untuk portal berbayar, bookmark adalah bagian dari nilai langganan โ ia milik akun,
bukan milik browser. Simpan di database, ambil lewat server island.
Menyimpan ke server dengan pembaruan optimistis
import { actions } from "astro:actions";
import { bookmark } from "@/stores/bookmark";
export async function toggleBookmark(id: number) {
const sebelum = bookmark.get();
// Optimistis: UI berubah seketika
bookmark.set(
sebelum.includes(id) ? sebelum.filter((x) => x !== id) : [...sebelum, id],
);
const { error } = await actions.toggleBookmark({ artikelId: id });
if (error) {
bookmark.set(sebelum); // kembalikan
tampilkanToast("Gagal menyimpan. Coba lagi.");
}
}
Pembaruan optimistis membuat antarmuka terasa instan meski jaringannya lambat. Syaratnya satu dan tidak bisa ditawar: harus ada jalur pengembalian saat gagal, dan pembaca harus diberi tahu. UI yang berubah lalu diam-diam kembali beberapa detik kemudian lebih membingungkan daripada UI yang menunggu.
Ukuran
| Pustaka | Ukuran (gzip, kira-kira) | Lintas framework |
|---|---|---|
| nanostores | < 1 KB | Ya |
| Pinia | ~2 KB (+ butuh instance app Vue) | Tidak |
| Vuex | ~3 KB | Tidak |
Selisih ukurannya kecil; yang menentukan adalah baris terakhir. Pinia butuh app.use() pada
instance aplikasi Vue โ dan tiap island punya instance sendiri, jadi tiap island akan mendapat store
sendiri. Nanostores hidup di luar framework mana pun, dan justru itu yang membuatnya bekerja di sini.
Latihan: buat store bookmark dengan persistentAtom, lalu dua island terpisah โ
tombol di atas halaman dan penghitung di header. Klik tombolnya dan pastikan penghitungnya ikut berubah.
Lalu muat ulang halaman dan pastikan datanya bertahan. Terakhir, sengaja tulis JSON rusak ke
localStorage lewat DevTools dan pastikan halamanmu tidak pecah.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.