Custom store adalah store yang kamu bungkus sendiri: sebuah function membuat writable di dalamnya, lalu cuma membuka subscribe dan method bernama yang kamu izinkan, misalnya tambah, hapus, dan kosongkan. Bayangin mesin kasir Warung Bu Sri. Pelayan cuma bisa menekan tombol "tambah item", "hapus item", dan "batal semua". Laci uangnya terkunci, jadi gak ada yang bisa mengisi angka sembarangan. Writable biasa itu laci yang terbuka; custom store itu mesin kasir dengan tombolnya.
🧪 Tebakan cepat:
keranjangadalahwritable([])biasa, dan Header menampilkanKeranjang: {$keranjang.length} menu. Sebuah komponen lain menjalankankeranjang.set("rusak"). Header jadi menampilkan apa? Jawabannya ada di bagian "Masalah writable biasa".
Masalah writable biasa
Di pelajaran Stores, keranjang adalah writable yang dibuka penuh: siapa pun bisa memanggil set dan update dengan nilai apa pun. Itu menimbulkan dua masalah:
- Logika tersebar. Aturan "kalau menunya sudah ada, tambah porsinya" ditulis di DaftarMenu. Aturan menghapus ditulis di Header. Komponen baru yang butuh harus menulisnya lagi, dan bisa saja menulisnya beda.
- Nilai sembarangan.
keranjang.set("rusak")diterima. Store-nya sekarang berisi string"rusak", dan$keranjang.lengthadalah panjang string itu, 5. Header menampilkanKeranjang: 5 menu, lalu{#each}membaca tiap hurufnya, jadi muncul lima barisundefined xundefined. Itu jawaban tebakannya. Gak ada error di saat itu juga; rusaknya baru ketahuan nanti.
createKeranjang(): bungkus logikanya
// keranjang.js
import { writable } from "svelte/store"
function createKeranjang() {
const { subscribe, set, update } = writable([])
return {
subscribe,
tambah: (nama) =>
update((isi) => {
const ada = isi.find((m) => m.nama === nama)
if (ada) return isi.map((m) => (m === ada ? { ...m, porsi: m.porsi + 1 } : m))
return [...isi, { nama, porsi: 1 }]
}),
hapus: (nama) => update((isi) => isi.filter((m) => m.nama !== nama)),
kosongkan: () => set([]),
}
}
export const keranjang = createKeranjang()
const { subscribe, set, update } = writable([])mengambil tiga method writable ke variabel biasa. Writable-nya sendiri gak pernah keluar dari function ini.- Objek yang dikembalikan cuma berisi
subscribedan tiga method buatanmu.setdanupdatedipakai di dalam, tapi gak ikut dikembalikan. tambah("Nasi Goreng")dua kali menghasilkan satu baris{ nama: "Nasi Goreng", porsi: 2 }, bukan dua baris. Aturan itu sekarang tinggal di satu tempat.- Membungkus logika dan menyembunyikan cara kerjanya di balik beberapa method seperti ini disebut enkapsulasi. Itu alasan custom store lebih cocok daripada writable biasa untuk logika yang rumit.
createKeranjang()dipanggil sekali, dan hasilnya diekspor. Semua komponen yang mengimpornya memakai keranjang yang sama.
Kontrak store: cukup subscribe
Kenapa $keranjang masih jalan, padahal objeknya bukan writable? Karena bagi Svelte, store adalah objek apa pun yang punya method subscribe: method yang memanggil callback dengan nilainya sekarang dan tiap berubah, lalu mengembalikan function untuk berhenti. Itu disebut kontrak store. Objek buatan sendiri juga sah:
<script>
const jamBuka = {
subscribe(fn) {
fn("07.00")
return () => {}
},
}
</script>
<p>Buka jam {$jamBuka}</p>
Hasilnya Buka jam 07.00. Sebaliknya, kalau createKeranjang lupa mengembalikan subscribe, {$keranjang} gagal dengan store.subscribe is not a function (di mode dev: 'keranjang' is not a store with a 'subscribe' method).
Memakai di komponen
<!-- DaftarMenu.svelte -->
<script>
import { keranjang } from "./keranjang.js"
const menu = ["Nasi Goreng", "Mie Ayam", "Es Teh"]
</script>
{#each menu as nama}
<button on:click={() => keranjang.tambah(nama)}>Tambah {nama}</button>
{/each}
<!-- Header.svelte -->
<script>
import { keranjang } from "./keranjang.js"
</script>
<p>Keranjang: {$keranjang.length} menu</p>
{#each $keranjang as m}
<p>{m.nama} x{m.porsi} <button on:click={() => keranjang.hapus(m.nama)}>Hapus</button></p>
{/each}
<button on:click={keranjang.kosongkan}>Kosongkan</button>
| Kejadian | Isi keranjang | Header |
|---|---|---|
| Tambah Nasi Goreng | Nasi Goreng x1 | Keranjang: 1 menu |
| Tambah Nasi Goreng lagi | Nasi Goreng x2 | Keranjang: 1 menu |
| Tambah Es Teh | Nasi Goreng x2, Es Teh x1 | Keranjang: 2 menu |
keranjang.set("rusak") |
(gak berubah) | TypeError: keranjang.set is not a function |
Komponen cuma tahu nama method-nya. Kalau aturan tambah diubah, misalnya maksimal 5 porsi, cukup ubah keranjang.js.
Latihan: createCounter
Latihannya memakai pola yang sama untuk penghitung:
<script>
import { writable } from "svelte/store"
function createCounter() {
const { subscribe, set, update } = writable(0)
return {
subscribe,
increment: () => update((n) => n + 1),
decrement: () => update((n) => n - 1),
reset: () => set(0),
}
}
const counter = createCounter()
</script>
<p>Count: {$counter}</p>
<button on:click={counter.increment}>+</button>
<button on:click={counter.decrement}>-</button>
<button on:click={counter.reset}>Reset</button>
Awalnya Count: 0. Klik + jadi Count: 1, klik Reset kembali Count: 0, dan dari awal, klik - jadi Count: -1. on:click={counter.increment} menyerahkan method-nya, tanpa tanda kurung, persis seperti handler di pelajaran Event Handling.
Svelte 5
Custom store tetap jalan di Svelte 5. Svelte 5 juga punya cara lain untuk membungkus state dan logikanya: class dengan field $state di file .svelte.js, misalnya class Keranjang { isi = $state([]); tambah(nama) { ... } }, lalu export const keranjang = new Keranjang(). Komponen membacanya sebagai keranjang.isi, tanpa $.
⚠️ Jebakan yang sering ketemu
- Lupa mengembalikan
subscribe. Objeknya bukan store lagi, dan{$keranjang}error.subscribewajib ada. - Mengembalikan writable-nya utuh.
return storeataureturn { ...store, tambah }ikut membukasetdanupdate, jadi siapa pun bisa mengisi"rusak"lagi. - Memanggil method di handler.
on:click={counter.increment()}menjalankan increment sekali saat komponen dibuat (langsungCount: 1), lalu kliknya gak berbuat apa-apa. Tulison:click={counter.increment}atauon:click={() => counter.increment()}. - Nama method yang beda. Latihannya memeriksa
increment,decrement, danresetpersis, jugasubscribe,writable(, danon:clickdi kodemu, dan butuh tiga tombol bertuliskan+,-, danReset.
TL;DR: Custom store adalah function seperti createKeranjang() yang membuat writable di dalamnya, lalu cuma mengembalikan subscribe plus method bernama (tambah, hapus, kosongkan). Logikanya tinggal di satu tempat (enkapsulasi), dan gak ada yang bisa memanggil set dari luar: keranjang.set("rusak") menghasilkan TypeError, sedangkan writable biasa menerimanya dan Header jadi Keranjang: 5 menu. Objek apa pun yang punya subscribe adalah store (kontrak store), jadi {$keranjang} tetap jalan; tanpa subscribe, error. writable memberi tiga method: subscribe, set, dan update. Latihannya: createCounter() dengan increment, decrement, dan reset, <p>Count: {$counter}</p>, dan tombol +, -, Reset yang menghasilkan Count: 1, Count: -1, dan Count: 0.