Pinia adalah library resmi Vue untuk menyimpan data yang dipakai banyak komponen di satu tempat bersama, di luar komponen mana pun. Tempat itu disebut store. Bayangin papan pesanan di dinding dapur Warung Bu Sri. Pelayan di depan menulis pesanan baru di papan, dan kasir di belakang membaca jumlahnya dari papan yang sama. Gak ada yang perlu mengoper kertas dari tangan ke tangan. Di aplikasi, ikon keranjang di header dan halaman menu sama-sama butuh isi keranjang; store adalah papan itu.
🧪 Tebakan cepat:
IkonKeranjangmenulisconst { jumlah } = useKeranjangStore(), lalu menampilkanKeranjang ({{ jumlah }}). DiHalamanMenu, pembeli menekan "Tambah Nasi Goreng" dua kali. Ikonnya menampilkan angka berapa? Tebak dulu, jawabannya ada di bagian "storeToRefs: mengambil sebagian store".
Kenapa butuh store
Sejauh ini data dibagi lewat props (turun ke child), emit (naik ke parent), provide/inject, atau composable yang menaruh ref-nya di luar fungsi supaya semua komponen memegang ref yang sama. Pinia adalah cara resmi menulis "satu tempat bersama" itu, plus devtools untuk melihat isinya. Sebuah store punya tiga bagian:
| Bagian store | Mirip | Isinya di keranjang |
|---|---|---|
| state | ref / reactive |
jumlah, total |
| getters | computed |
kosong, dihitung dari jumlah |
| actions | fungsi | tambah(harga), yang mengubah state |
Memasang Pinia dan menulis store pertama
Pasang sekali di proyek (npm install pinia), lalu daftarkan di main.js:
import { createApp } from "vue"
import { createPinia } from "pinia"
import App from "./App.vue"
const app = createApp(App)
app.use(createPinia())
app.mount("#app")
Store ditulis di filenya sendiri dengan defineStore():
// stores/keranjang.js
import { defineStore } from "pinia"
export const useKeranjangStore = defineStore("keranjang", {
state: () => ({ jumlah: 0, total: 0 }),
getters: {
kosong: (state) => state.jumlah === 0,
},
actions: {
tambah(harga) {
this.jumlah++
this.total += harga
},
},
})
defineStore("keranjang", ...)membuat store dengan idkeranjang. Id-nya harus unik di seluruh aplikasi.stateadalah fungsi yang mengembalikan nilai awal.- Getter menerima
statedan menghitung sesuatu darinya, persis seperticomputed. - Di dalam action,
thisadalah store-nya, jadithis.jumlah++mengubah state. - Hasil
defineStoreadalah fungsi. Namanya dibiasakanuse...Store:useKeranjangStore.
Memakai store di dua komponen
<!-- IkonKeranjang.vue -->
<script setup>
import { useKeranjangStore } from "../stores/keranjang"
const keranjang = useKeranjangStore()
</script>
<template>
<span>Keranjang ({{ keranjang.jumlah }})</span>
</template>
<!-- HalamanMenu.vue -->
<script setup>
import { useKeranjangStore } from "../stores/keranjang"
const keranjang = useKeranjangStore()
</script>
<template>
<button @click="keranjang.tambah(15000)">Tambah Nasi Goreng</button>
<p v-if="keranjang.kosong">Keranjang masih kosong</p>
<p v-else>{{ keranjang.jumlah }} item, Rp{{ keranjang.total }}</p>
</template>
| Kejadian | State store | Ikon | Halaman menu |
|---|---|---|---|
| Halaman tampil | jumlah: 0, total: 0 |
Keranjang (0) | Keranjang masih kosong |
| Tambah Nasi Goreng | jumlah: 1, total: 15000 |
Keranjang (1) | 1 item, Rp15000 |
| Tambah Nasi Goreng lagi | jumlah: 2, total: 30000 |
Keranjang (2) | 2 item, Rp30000 |
- Kedua komponen memanggil
useKeranjangStore()dan mendapat objek yang sama. Tombol di halaman menu mengubah state, ikon di header ikut berubah, tanpa props atau emit di antara keduanya. - Di template, state, getter, dan action dibaca langsung dari store:
keranjang.jumlah,keranjang.kosong,keranjang.tambah(15000).
Gaya setup: ref, computed, fungsi
Store yang sama bisa ditulis seperti <script setup>: berikan fungsi ke defineStore, lalu kembalikan semua yang mau dibagikan.
import { ref, computed } from "vue"
import { defineStore } from "pinia"
export const useKeranjangStore = defineStore("keranjang", () => {
const jumlah = ref(0)
const total = ref(0)
const kosong = computed(() => jumlah.value === 0)
function tambah(harga) {
jumlah.value++
total.value += harga
}
return { jumlah, total, kosong, tambah }
})
refjadi state,computedjadi getter, fungsi jadi action. Komponen memakainya persis sama.- Semua state harus ikut di-
return. Ref yang gak dikembalikan bukan bagian store. - Editor latihan memuat Pinia asli begitu kodemu memakai
defineStore, dan memasangcreatePinia()baru setiap kali pratinjau dijalankan, jadi kamu gak perlu menulisapp.use(createPinia()). Di latihan pertama, kamu melengkapi store counter gaya setup:count = ref(0)sebagai state,doubleCount = computed(() => count.value * 2)sebagai getter,incrementsebagai action. Tombolnya ada di satu komponen dan angkanya di komponen lain, jadi kamu lihat sendiri dua komponen berbagi satu store. Latihan kedua membuat store keranjang sendiri:itemsmulai dari0,isEmptysebagai computed yang memberi"Ya"saatitemsmasih 0 dan"Tidak"sesudahnya.
storeToRefs: mengambil sebagian store
Store adalah objek reactive. Kalau di-destructure, yang diambil cuma angkanya saat itu, bukan sambungannya.
const { jumlah } = useKeranjangStore() // salah: angka biasa, beku di 0
const { jumlah } = storeToRefs(useKeranjangStore()) // benar: ref yang tetap tersambung
IkonKeranjang menulis |
Sesudah Tambah dua kali, ikon | Halaman menu |
|---|---|---|
const keranjang = useKeranjangStore() |
Keranjang (2) | 2 item, Rp30000 |
const { jumlah } = useKeranjangStore() |
Keranjang (0) | 2 item, Rp30000 |
const { jumlah } = storeToRefs(useKeranjangStore()) |
Keranjang (2) | 2 item, Rp30000 |
Ikon dengan destructuring biasa tetap Keranjang (0). Itu jawaban tebakannya. Store-nya benar berisi 2, halaman menu juga menampilkan 2, tapi ikon memegang angka 0 yang disalin saat ia dibuat. storeToRefs (di-import dari "pinia") mengubah state dan getter jadi ref, jadi tetap tersambung. Action boleh di-destructure langsung tanpa storeToRefs: const { tambah } = keranjang tetap mengubah store yang sama.
📌 Store dipakai sebelum app.use(pinia)
Kalau useKeranjangStore() dipanggil sebelum Pinia didaftarkan (misalnya app.use(createPinia()) lupa ditulis), build development langsung berhenti dengan pesan ini:
[🍍]: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?
Build production gak menulis pesan itu dan cuma gagal dengan Cannot read properties of undefined (reading '_s'). Obatnya sama: app.use(createPinia()) sebelum app.mount(), dan panggil store di dalam komponen atau fungsi, bukan di baris paling atas sebuah file.
⚠️ Jebakan yang sering ketemu
- Destructuring store.
const { jumlah } = useKeranjangStore()kehilangan reaktivitas. PakaistoreToRefs()untuk state dan getter, atau tuliskeranjang.jumlah. - Lupa
app.use(createPinia()). Store gak punya tempat tinggal, dan aplikasinya berhenti. - State ditulis sebagai objek, bukan fungsi. Pinia memanggil
state(), jadistate: { jumlah: 0 }gagal denganstate is not a functiondi build development. Tulisstate: () => ({ jumlah: 0 }). - Gaya setup yang lupa
return. Ref yang gak dikembalikan gak ikut jadi state store. - Getter yang disalin manual. Di latihan,
doubleCountharuscomputed(() => count.value * 2), bukan angka yang ditulis terpisah, supaya ikut berubah saatcountnaik.
TL;DR: Pinia menyimpan data bersama di store, di luar komponen. Pasang dengan app.use(createPinia()), tulis store dengan defineStore("id", { state, getters, actions }) atau gaya setup defineStore("id", () => { ...; return { ... } }), di mana ref jadi state, computed jadi getter, dan fungsi jadi action. Setiap komponen yang memanggil useKeranjangStore() mendapat objek yang sama, jadi perubahan dari satu komponen langsung terlihat di komponen lain. Jangan destructure store langsung: pakai storeToRefs() untuk state dan getter; action boleh diambil langsung.