โ† Semua pembelajaran / Astro Nol โ†’ Portal Berita
Fase 4 ยท Island Vue & Interaktivitas

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.

Sumber asli docs.astro.build Resmi Rangkuman ~8 menit baca

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 localStorage atau 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?

SituasiJawaban
Dua island bersebelahan di DOMGabungkan jadi satu island. Paling sederhana, paling cepat
State-nya cuma UI: tab aktif, panel terbukaSimpan lokal di komponennya
State-nya layak dibagikan lewat tautanURL, bukan store
Preferensi per perangkat: tema, ukuran fontpersistentAtom
Berlaku di semua perangkat: bookmark, langgananServer. localStorage tidak ikut pindah perangkat
Island berjauhan tapi harus sinkron dalam satu halamanNanostores

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

PustakaUkuran (gzip, kira-kira)Lintas framework
nanostores< 1 KBYa
Pinia~2 KB (+ butuh instance app Vue)Tidak
Vuex~3 KBTidak

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.