Vue Pinia
Target: pemula yang sudah memahami dasar Vue 3 + Composition API (component, props, emit,
ref,computed), lalu ingin mengenal state management dengan Pinia. Versi: Pinia 2.x / Vue 3.x Prasyarat: Vue Dasar Fokus modul pembelajaran ini: pengenalan → setup → store → state → reset → storeToRefs → getter → action → global state → setup store → store composition → onAction → typescript → ssr → plugins → mini project. Semua contoh utama menggunakan<script setup>dan Option Store, lalu diperdalam dengan Setup Store agar mudah membandingkan keduanya.
Cara Belajar
🟢 Fundamental
→ wajib dipahami untuk mulai mengelola state bersama antar-komponen
🟡 Lanjutan
→ pelajari setelah konsep dasar store nyaman
🔴 Advanced / Reference
→ penting ketika kebutuhan aplikasi meningkatMental model:
Pinia Store (Global State)
┌────────────────────────────────┐
│ │
│ State (data / ref) │
│ │ │
│ ▼ │
│ Getters (computed) │
│ │ │
│ ▼ │
│ Actions (methods / functions)│
│ │
└────────────────────────────────┘
▲ │
read/write │ │ reactivity
│ ▼
┌──────────────────┐
│ Vue Components │
│ (Navbar, Card, │
│ Checkout, dll.) │
└──────────────────┘Hafalan:
State → data utama
Getter → nilai turunan (computed)
Action → fungsi / methods
Store → tempat ketiganyaDaftar Isi
🟢 Fundamental
- Pengenalan Pinia
- Setup & Instalasi
- Membuat Store
- State
- Mengubah State dengan
$patch dan $reset - Destructuring dengan storeToRefs
- Getters
- Actions
🟡 Lanjutan
- Global State & Pola Akses Komponen
- Setup Store (Composition API Style)
- Store Composition (Akses Lintas Store)
- Mengamati Store (
$subscribe dan $onAction) - Pinia dengan TypeScript
🔴 Advanced / Reference
- Store di Luar Component & SSR
- Pinia Plugins & Persist State
- Peta Ingatan Cepat
- Tabel Ringkasan
- Cheat Code Pinia 10 Detik
- Urutan Belajar yang Disarankan
- Mini Project: Shopping Cart Terpadu
- Referensi Resmi
1. 🟢 Pengenalan Pinia
Konsep
Pinia adalah state management library resmi untuk Vue.
Sederhananya:
Component A ──┐
│
Component B ──┼──> Pinia Store
│ │
Component C ──┘ ▼
Shared StateTanpa store, komunikasi data antar-component bisa menjadi panjang (prop drilling):
Parent Component
│
│ props: { user }
▼
Child Component
│
│ props: { user }
▼
Grandchild Component
│
│ props: { user } (hanya perantara)
▼
UserBadge Component <── Butuh data user di sini!Dengan Pinia:
Component A ─────┐
│
Component B ─────┼──> Store
│ │
Component C ─────┘ ▼
State bersamaPinia store menyimpan state dan business logic yang tidak terikat pada satu component tree. Konsep utamanya adalah state, getters, dan actions, yang dapat dipahami seperti data, computed, dan methods pada component Vue.
Kapan menggunakan Pinia?
Cocok untuk data yang:
dipakai banyak component
harus bertahan ketika berpindah halaman
memiliki business logic bersamaContoh data yang tepat masuk ke Pinia:
Sesi Login Pengguna (User Auth)
Keranjang Belanja (Shopping Cart)
Preferensi Tema (Dark / Light Mode)
Notifikasi Global (Toast / Alerts)
Filter Pencarian Produk Lintas HalamanTidak semua data harus masuk store. Data yang hanya digunakan satu component lebih baik tetap menjadi local state:
Modal sedang terbuka? ──> Local State (ref di dalam .vue)
Input form sementara? ──> Local State (ref di dalam .vue)
Tab aktif halaman ini? ──> Local State (ref di dalam .vue)Model mental Pinia
Pinia Store
┌─────────────────┐
│ │
│ State │
│ │ │
│ ▼ │
│ Getters │
│ │ │
│ ▼ │
│ Actions │
│ │
└─────────────────┘
▲ │
read/write │ │ reactivity
│ ▼
ComponentsHafalan:
State → data
Getter → computed
Action → method
Store → tempat ketiganyaKunci: Mulai dengan local state. Masukkan ke Pinia hanya ketika data benar-benar dipakai banyak component atau harus bertahan lintas halaman.
2. 🟢 Setup & Instalasi
Konsep
Pinia didaftarkan ke aplikasi Vue sebagai plugin dengan createPinia().
Install Pinia
npm install piniaPasang Pinia
src/main.js:
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')Diagram Alur Setup
createApp(App)
│
│ inisialisasi aplikasi Vue
▼
createPinia()
│
│ buat root instance Pinia
▼
app.use(pinia)
│
│ daftarkan plugin ke seluruh komponen
▼
app.mount('#app')
│
▼
Pinia Aktif & Siap DigunakanSetelah app.use(pinia), store Pinia dapat digunakan oleh component di aplikasi.
Struktur folder
src/
├── components/
├── views/
├── stores/ <── Folder khusus store
│ ├── counter.js
│ ├── user.js
│ └── cart.js
├── App.vue
└── main.jsBiasanya setiap store diletakkan pada file terpisah:
stores/
│
├── user.js
├── cart.js
└── product.jsPinia mendukung banyak store dan pola modular.
Hafalan:
createPinia() → app.use(pinia) → store tersedia di aplikasiBest Practice: Simpan setiap store di file terpisah dalam folder src/stores/.
3. 🟢 Membuat Store
Konsep
Store adalah tempat untuk menyimpan state dan logic yang ingin digunakan bersama. Store dibuat menggunakan defineStore() dan membutuhkan id unik. Konvensi nama function store biasanya use...Store, misalnya useUserStore atau useCartStore.
Membuat Store
src/stores/counter.js:
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})Diagram Anatomi Store
┌───────────────────────────────────────┐
│ useCounterStore (ID: 'counter') │
├───────────────────────────────────────┤
│ State : count = 0 │
│ Getter : doubleCount (count * 2) │
│ Action : increment() │
└───────────────────┬───────────────────┘
│
│ useCounterStore()
▼
┌───────────────────────────────────────┐
│ Component │
│ <p>{{ counter.count }}</p> │
│ <button @click="counter.increment()">│
└───────────────────────────────────────┘Menggunakan Store
Di component:
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment()">
Tambah
</button>
</template>Output awal
Count: 0
Double: 0
[ Tambah ]Klik tombol [ Tambah ]:
Count: 1
Double: 2
[ Tambah ]Store adalah reactive
Instance store dapat dibaca dan diubah langsung:
const counter = useCounterStore()
counter.count++Pinia memang dirancang agar state pada store dapat diakses langsung tanpa wrapper verbose seperti pada pola Vuex lama.
Hafalan:
defineStore('id', { ... }) → membuat definisi store
useCounterStore() → mengambil instance store di komponenKesalahan Umum:
❌ Memanggil defineStore() di dalam component.
✅ defineStore() dipanggil di file store, lalu useStore() dipanggil di component.
4. 🟢 State
Konsep
State adalah data utama yang disimpan oleh store. Pada Option Store, state didefinisikan sebagai function yang mengembalikan object initial state.
Membuat State
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: 'Budi',
age: 20,
isLoggedIn: false,
items: [],
}),
})Membaca State
<script setup>
import { useUserStore } from '@/stores/user'
const user = useUserStore()
</script>
<template>
<p>{{ user.name }}</p>
<p>{{ user.age }}</p>
</template>Output
Budi
20Mengubah State Langsung
State dapat diubah langsung dari component:
const user = useUserStore()
user.name = 'Andi'
user.age = 21Diagram Alur Mutasi State
user.name = 'Andi'
│
│ mutasi langsung pada proxy store
▼
State di Pinia Berubah
│
│ melacak dependensi komponen
▼
DOM Komponen Diperbarui OtomatisState yang belum memiliki nilai
Tetap deklarasikan property sejak awal:
state: () => ({
user: null,
items: [],
})Jangan menambahkan property yang tidak pernah didefinisikan di state():
// ❌ Jangan lakukan ini
store.newProperty = 'hello'Hafalan:
state: () => ({ semuaStateAwal })
→ semua state wajib dideklarasikan sejak awal agar dapat dilacakKesalahan Umum:
❌ Menambahkan property baru ke store di luar fungsi state().
✅ Deklarasikan semua state di awal, termasuk yang nilainya masih null atau [].
5. 🟢 Mengubah State dengan $patch dan $reset
Konsep
Pinia menyediakan method bawaan $patch() untuk mengubah beberapa state sekaligus dalam 1 kali batch update dan $reset() untuk mengembalikan state ke kondisi awal.
Menggunakan $patch dengan Objek
const user = useUserStore()
user.$patch({
name: 'Andi',
age: 21,
})Menggunakan $patch dengan Fungsi Callback
Untuk perubahan yang melibatkan array atau mutasi bersyarat:
user.$patch((state) => {
state.items.push({ id: 1, text: 'Buku' })
state.age++
state.isLoggedIn = true
})Diagram alur $patch:
Mutasi A (name) ──┐
Mutasi B (age) ──┼──> $patch() ──> 1x Batch Update ke DOM
Mutasi C (items) ──┘Reset State: $reset()
Pada Option Store, Pinia menyediakan method $reset() untuk mengembalikan nilai ke kondisi default saat store dibuat:
const user = useUserStore()
// State kembali ke name: 'Budi', age: 20, isLoggedIn: false
user.$reset()Diagram Alur $reset
user.name ('Andi') ──┐
│ ──> user.$reset() ──> Initial State (Budi, 20)
user.age (21) ──┘Hafalan:
$patch() → ubah banyak state sekaligus dalam 1 batch
$reset() → kembalikan seluruh state ke nilai awal6. 🟢 Destructuring dengan storeToRefs
Konsep
Ketika kita melakukan destructuring langsung pada objek store, reaktivitas state dan getter akan hilang:
const store = useCounterStore()
// ❌ SALAH: count dan doubleCount menjadi variabel biasa (bukan reactive lagi)
const { count, doubleCount } = storeAgar destructuring tetap mempertahankan sifat reactive, gunakan storeToRefs().
Cara Penggunaan yang Benar
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// ✅ BENAR: state dan getter dibungkus storeToRefs
const { count, doubleCount } = storeToRefs(counter)
// ✅ Action di-destructure LANGSUNG tanpa storeToRefs
const { increment } = counter
</script>
<template>
<p>{{ count }} / {{ doubleCount }}</p>
<button @click="increment">Tambah</button>
</template>Diagram Ekstraksi Reaktif
┌───────────────────────────────┐
│ useCounterStore() │
└───────┬───────────────┬───────┘
│ │
state/getters │ │ actions
▼ ▼
┌───────────────┐ ┌───────────────┐
│ storeToRefs() │ │ Destructure │
│ │ │ Langsung │
└───────┬───────┘ └───────┬───────┘
│ │
▼ ▼
Tetap Reaktif Fungsi Action
(ref.value) (method)Hafalan:
storeToRefs(store) → untuk state & getter
action → ambil langsung dari storeKesalahan Umum:
❌ Membungkus action ke dalam storeToRefs(store).
✅ storeToRefs hanya untuk data reaktif (state dan getter).
7. 🟢 Getters
Konsep
Getter adalah nilai turunan dari state, konsepnya mirip dengan computed pada component Vue. Getter otomatis di-cache dan hanya dihitung ulang jika dependensinya berubah.
Getter sederhana
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 10,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
})Component:
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
</template>Output
Count: 10
Double: 20Getter menggunakan this
Jika getter ingin mengakses getter lain, gunakan fungsi biasa (bukan arrow function):
getters: {
doubleCount: (state) => state.count * 2,
quadrupleCount() {
return this.doubleCount * 2
},
}Hasil:
count = 10
doubleCount = 20
quadrupleCount = 40Getter yang menerima parameter (Return a Function)
Getter dapat mengembalikan fungsi jika membutuhkan argumen dinamis:
getters: {
getTodoById: (state) => {
return (id) => state.todos.find(todo => todo.id === id)
},
}Di component:
<script setup>
const todoStore = useTodoStore()
const targetTodo = todoStore.getTodoById(2)
</script>Diagram Alur Getter
State Utama (count = 10)
│
│ hitung turunan data
▼
Getter doubleCount (computed: count * 2)
│
│ cache otomatis hasil = 20
▼
Tampilan Komponen (UI Render)Getter bukan tempat side effect
getter → hitung / turunkan nilai (pure function)
action → jalankan logic / mutasi / side effectHafalan:
State → Getter → derived value
Getter = computed milik storeKesalahan Umum:
❌ Menjalankan side effect (API request, localStorage) di dalam getter.
✅ Getter hanya untuk menghitung dan mengembalikan nilai.
8. 🟢 Actions
Konsep
Action adalah function yang berisi logic atau business logic pada store. Action setara dengan method pada component Vue, dan dapat menggunakan this untuk mengakses state, getter, dan action lain.
Membuat Action
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
add(amount) {
this.count += amount
},
},
})Diagram Siklus Action
┌───────────────────────────────┐
│ Component │
└───────────────┬───────────────┘
│
│ store.increment()
▼
┌───────────────────────────────┐
│ Pinia Action │
│ (this.count++) │
└───────────────┬───────────────┘
│
│ mutasi data
▼
┌───────────────────────────────┐
│ Reactive State │
└───────────────┬───────────────┘
│
│ reaktivitas
▼
┌───────────────────────────────┐
│ UI Update │
└───────────────────────────────┘Action Asynchronous (API Call)
Action dapat berupa async function dan cocok untuk API request:
actions: {
async fetchUsers() {
this.isLoading = true
try {
const response = await fetch('/api/users')
this.users = await response.json()
} catch (error) {
this.error = error.message
} finally {
this.isLoading = false
}
},
}Action memanggil Action lain
actions: {
increment() {
this.count++
},
incrementTwice() {
this.increment()
this.increment()
},
}Hafalan:
Action → logic → ubah state → UI updateBest Practice: Letakkan semua logic perubahan state yang kompleks (termasuk async) di action, bukan di dalam komponen.
9. 🟡 Global State & Pola Akses Komponen
Konsep
Salah satu alasan utama menggunakan Pinia adalah membuat state yang dapat digunakan oleh banyak component secara konsisten tanpa jalur props bertingkat.
Tanpa Pinia (Prop Drilling Panjang)
┌───────────────────────────┐
│ App.vue │
└─────────────┬─────────────┘
│
│ props: { user }
▼
┌───────────────────────────┐
│ Layout.vue │
└─────────────┬─────────────┘
│
│ props: { user }
▼
┌───────────────────────────┐
│ Header.vue │
└─────────────┬─────────────┘
│
│ props: { user }
▼
┌───────────────────────────┐
│ UserMenu.vue │
└───────────────────────────┘Dengan Pinia (Pusat Data Bersama)
┌───────────────────────────────────────────┐
│ useUserStore │
│ (Global State) │
└───────┬─────────────┬─────────────┬───────┘
│ │ │
useUser() │ useUser() │ useUser() │
▼ ▼ ▼
┌──────────────┐┌──────────────┐┌──────────────┐
│ Navbar.vue ││Dashboard.vue ││ Profile.vue │
└──────────────┘└──────────────┘└──────────────┘Semua component mengambil store yang sama:
const user = useUserStore()Contoh User Store
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: 'Budi',
isLoggedIn: true,
}),
getters: {
greeting: (state) => `Halo ${state.name}`,
},
actions: {
login(name) {
this.name = name
this.isLoggedIn = true
},
logout() {
this.name = ''
this.isLoggedIn = false
},
},
})Jika action dijalankan dari salah satu component:
user.login('Andi')maka semua component lain yang membaca state tersebut otomatis menerima pembaruan secara reactive.
Membedakan Local State vs Global State
Data hanya dibutuhkan oleh 1 komponen?
│
▼
Gunakan LOCAL STATE (const state = ref() di dalam .vue)
Data dibutuhkan oleh banyak komponen / lintas halaman?
│
▼
Gunakan GLOBAL STATE (Pinia Store di src/stores/)Hafalan:
Local state → untuk komponen tunggal (isModalOpen, activeTab)
Global state → untuk data bersama (currentUser, cart, theme)10. 🟡 Setup Store (Composition API Style)
Konsep
Pinia menyediakan sintaks Setup Store yang identik dengan gaya Vue 3 <script setup>:
ref()mewakili Statecomputed()mewakili Gettersfunction()mewakili Actions
Contoh Setup Store
src/stores/counterSetup.js:
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterSetupStore = defineStore('counterSetup', () => {
// State
const count = ref(0)
// Getter
const doubleCount = computed(() => count.value * 2)
// Action
function increment() {
count.value++
}
function decrement() {
count.value--
}
// Wajib me-return semua yang ingin diekspos
return {
count,
doubleCount,
increment,
decrement,
}
})Pemetaan Option Store vs Setup Store
Option Store Setup Store
──────────── ───────────
state: () => ({}) ──> ref()
getters: {} ──> computed()
actions: {} ──> function()Perbandingan
//Option Store
defineStore('id', {
state,
getters,
actions
})
// → sangat terstruktur, mudah dipelajari pemula
//Setup Store
defineStore('id', () => {
ref()
computed()
function()
return { ... }
})
// → lebih fleksibel, cocok untuk pengguna Composition APIHafalan:
ref() → state
computed() → getter
function() → action11. 🟡 Store Composition (Akses Lintas Store)
Konsep
Pinia memungkinkan sebuah store menggunakan store lain secara langsung di dalam Getters maupun Actions.
Contoh: Cart Store menggunakan User Store
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
}),
actions: {
async checkout() {
const user = useUserStore()
// Validasi data dari store lain
if (!user.isLoggedIn) {
throw new Error('Harap login terlebih dahulu')
}
console.log(`Memproses checkout untuk ${user.name}...`)
},
},
})Diagram Hubungan Antar-Store
┌────────────────────────────┐
│ useUserStore │
│ user: { isLoggedIn: true }│
└─────────────┬──────────────┘
│
│ dibaca oleh action
▼
┌───────────────────────────┐
│ useCartStore │
│ checkout() { ... } │
└─────────────┬─────────────┘
│
│ validasi sukses
▼
┌───────────────────────────┐
│ Proses Transaksi Belanja│
└───────────────────────────┘Hafalan:
Import useStoreLain() → panggil di dalam action/getter saat dibutuhkan12. 🟡 Mengamati Store ($subscribe dan $onAction)
Konsep
Pinia menyediakan method untuk mengamati perubahan state ($subscribe) dan siklus hidup pemanggilan action ($onAction).
Mengamati Perubahan State: $subscribe
const counter = useCounterStore()
counter.$subscribe((mutation, state) => {
console.log('Tipe mutasi:', mutation.type)
console.log('State terbaru:', state.count)
})Diagram Alur $subscribe
State berubah
↓
$subscribe terpanggil
↓
Sync ke LocalStorage / LoggingMengamati Action: $onAction
counter.$onAction(({ name, args, after, onError }) => {
console.log(`Action [${name}] dimulai dengan argumen:`, args)
after((result) => {
console.log(`Action [${name}] selesai dengan hasil:`, result)
})
onError((error) => {
console.error(`Action [${name}] gagal:`, error)
})
})Diagram Siklus $onAction
Komponen Memanggil Action
│
▼
onAction Listener Aktif
│
┌─────────┴─────────┐
│ │
▼ ▼
after() onError()
(Aksi Berhasil) (Aksi Gagal)Hafalan:
$subscribe → mengamati mutasi state
$onAction → mengamati pemanggilan action (before, after, error)13. 🟡 Pinia dengan TypeScript
Konsep
Pinia memiliki inferensi tipe otomatis yang sangat baik untuk TypeScript.
Menentukan Tipe State
import { defineStore } from 'pinia'
interface UserItem {
id: number
name: string
role: 'admin' | 'user'
}
interface UserState {
users: UserItem[]
currentUser: UserItem | null
isLoading: boolean
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
users: [],
currentUser: null,
isLoading: false,
}),
actions: {
setUser(user: UserItem) {
this.currentUser = user
},
},
})TypeScript otomatis menginferensikan return type pada state dan getters.
Hafalan:
state: (): UserState => ({ ... })
→ tentukan interface state awal agar autocomplete bekerja maksimal14. 🔴 Store di Luar Component & SSR
Konsep
Store Pinia dapat digunakan di luar file komponen .vue, misalnya di dalam file Vue Router.
Penggunaan di Vue Router Guard
Pastikan store dipanggil di dalam callback router, bukan di top-level file:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/dashboard', component: () => import('@/views/Dashboard.vue'), meta: { requiresAuth: true } },
],
})
// ✅ Panggil useUserStore() di dalam hook
router.beforeEach((to, from, next) => {
const user = useUserStore()
if (to.meta.requiresAuth && !user.isLoggedIn) {
next('/login')
} else {
next()
}
})
export default routerDiagram Alur Router Guard
Navigasi Route (to: '/dashboard')
│
│ router.beforeEach()
▼
useUserStore() Aktif
│
│ periksa status isLoggedIn
▼
Pengecekan Otorisasi
│ │
Sudah Login Belum Login
│ │
▼ ▼
next() next('/login')Hafalan:
Panggil useStore() di dalam fungsi router guard, bukan di luar fungsi15. 🔴 Pinia Plugins & Persist State
Konsep
Plugin Pinia digunakan untuk memperluas fungsi store, seperti auto-save ke localStorage. Plugin dipasang menggunakan pinia.use().
1. Plugin Sederhana (Custom Logger)
import { createPinia } from 'pinia'
const pinia = createPinia()
function loggerPlugin({ store }) {
store.$subscribe((mutation, state) => {
console.log(`[Plugin] Store ${store.$id} berubah:`, state)
})
}
pinia.use(loggerPlugin)2. Persist State dengan pinia-plugin-persistedstate
Install plugin:
npm install pinia-plugin-persistedstatePasang di src/main.js:
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)Aktifkan di store:
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
}),
persist: true, // Otomatis tersimpan di localStorage!
})Diagram Alur Persistensi
State di Store Berubah
│
│ pinia.use(plugin) mendeteksi mutasi
▼
Plugin Persistedstate Aktif
│
│ serialisasi state ke JSON
▼
localStorage Browser TerupdateHafalan:
pinia.use(plugin) → daftarkan plugin
persist: true → auto-save state ke browser storage16. 🧠 Peta Ingatan Cepat
A. Konsep Dasar
Pinia
│
┌───────┼────────┐
│ │ │
▼ ▼ ▼
State Getter Action
│ │ │
▼ ▼ ▼
data computed methodsB. Alur Data Komponen & Store
┌───────────────────────────────┐
│ Component │
└───────────────┬───────────────┘
│
│ 1. panggil action
▼
┌───────────────────────────────┐
│ Pinia Action │
└───────────────┬───────────────┘
│
│ 2. ubah state
▼
┌───────────────────────────────┐
│ State Utama │
└───────┬───────────────┬───────┘
│ │
│ 3. komputasi │ 4. trigger reaktif
▼ ▼
┌───────────────┐ ┌─────────────┐
│ Getter │ │ UI Render │
└───────────────┘ └─────────────┘C. Setup
createPinia()
↓
app.use(pinia)
↓
useStore()
↓
store tersediaD. Store
defineStore('id', ...)
│
▼
useSomethingStore
│
▼
const store = useSomethingStore()E. State & Patch
state
↓
data reactive
store.count
↓
baca / ubah langsung
store.$patch()
↓
ubah banyak state sekaligus
store.$reset()
↓
kembali ke initial stateF. Destructuring dengan storeToRefs
┌───────────────────────────────┐
│ Store │
└───────┬───────────────┬───────┘
│ │
state/getters │ │ actions
▼ ▼
┌───────────────┐ ┌───────────────┐
│ storeToRefs() │ │ Destructure │
│ │ │ Langsung │
└───────┬───────┘ └───────┬───────┘
│ │
▼ ▼
Ref Reaktif Fungsi ActionG. Option Store vs Setup Store
Option Store Setup Store
──────────── ───────────
state ref
getters computed
actions function17. 📚 Tabel Ringkasan
| Materi | Fungsi | Kata Kunci |
|---|---|---|
| Pengenalan | State management resmi Vue | Pinia |
| Setup | Memasang Pinia di main.js | createPinia() |
| Store | Tempat penyimpanan state + logic | defineStore() |
| State | Data reactive utama | state: () => ({}) |
$patch | Mengubah banyak state dalam 1 batch | $patch() |
$reset | Mengembalikan state ke nilai awal | $reset() |
storeToRefs | Destructure state & getter tetap reaktif | storeToRefs() |
| Getter | Nilai komputasi turunan | getters: {} |
| Action | Business logic & mutasi state | actions: {} |
| Global State | Akses data store lintas komponen | useStore() |
| Setup Store | Penulisan store bergaya Composition API | defineStore('id', () => {}) |
| Store Composition | Store menggunakan store lain | useOtherStore() |
$subscribe | Mengamati mutasi state | $subscribe() |
$onAction | Mengamati siklus action | $onAction() |
| TypeScript | Type inference & static typing | UserState |
| Plugins | Ekstensi fungsionalitas store | pinia.use() |
18. ⚡ Cheat Code Pinia 10 Detik
createPinia() → memasang Pinia ke aplikasi
defineStore('id') → membuat store
state → data reactive
getters → nilai turunan (computed)
actions → method + business logic
useStore() → mengambil instance store
storeToRefs() → destructure tetap reactive
$patch() → ubah banyak state sekaligus
$reset() → kembali ke state awalSetup dasar
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')Store dasar
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})Menggunakan store di komponen
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
const { count, double } = storeToRefs(counter)
const { increment } = counter
</script>
<template>
<p>{{ count }} / {{ double }}</p>
<button @click="increment">Tambah</button>
</template>19. 🧭 Urutan Belajar yang Disarankan
1. Pengenalan
↓
2. Setup
↓
3. Store
↓
4. State
↓
5. $patch & $reset
↓
6. storeToRefs()
↓
7. Getter
↓
8. Action
↓
9. Global State
↓
10. Setup Store
↓
11. Store Composition
↓
12. $subscribe / $onAction
↓
13. TypeScript
↓
14. Plugins & Persistence
↓
15. Mini ProjectPrinsip: Kuasai dulu konsep dasar State → Getter → Action dengan Option Store, baru pelajari Setup Store dan fitur lanjutan.
20. 🏗️ Mini Project: Shopping Cart Terpadu
Contoh mini project yang menggabungkan: State, Getters, Actions, storeToRefs, dan Cross-Component access.
1. Cart Store (src/stores/cart.js)
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [
{ id: 1, text: 'Buku Vue 3', price: 100000, qty: 1 },
{ id: 2, text: 'Stiker Pinia', price: 15000, qty: 2 },
],
}),
getters: {
totalItems: (state) => {
return state.items.reduce((total, item) => total + item.qty, 0)
},
totalPrice: (state) => {
return state.items.reduce((total, item) => total + (item.price * item.qty), 0)
},
},
actions: {
addItem(text, price) {
if (!text.trim()) return
this.items.push({
id: Date.now(),
text,
price: Number(price),
qty: 1,
})
},
removeItem(id) {
this.items = this.items.filter(item => item.id !== id)
},
clearCart() {
this.items = []
},
},
})2. Komponen Antarmuka (src/App.vue)
<script setup>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
const { items, totalItems, totalPrice } = storeToRefs(cart)
const { addItem, removeItem, clearCart } = cart
const newItemName = ref('')
const newItemPrice = ref(0)
function handleAdd() {
if (!newItemName.value || newItemPrice.value <= 0) return
addItem(newItemName.value, newItemPrice.value)
newItemName.value = ''
newItemPrice.value = 0
}
</script>
<template>
<div class="cart-app">
<h1>Mini Shopping Cart</h1>
<div class="form">
<input v-model="newItemName" placeholder="Nama barang..." />
<input v-model.number="newItemPrice" type="number" placeholder="Harga..." />
<button @click="handleAdd">Tambah Barang</button>
</div>
<p>Total Barang: {{ totalItems }}</p>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.text }} (x{{ item.qty }}) — Rp {{ (item.price * item.qty).toLocaleString('id-ID') }}
<button @click="removeItem(item.id)">Hapus</button>
</li>
</ul>
<h3>Total Bayar: Rp {{ totalPrice.toLocaleString('id-ID') }}</h3>
<button @click="clearCart">Kosongkan Keranjang</button>
</div>
</template>Output
Mini Shopping Cart
[ Nama barang... ] [ Harga... ] [ Tambah Barang ]
Total Barang: 3
• Buku Vue 3 (x1) — Rp 100.000 [ Hapus ]
• Stiker Pinia (x2) — Rp 30.000 [ Hapus ]
Total Bayar: Rp 130.000
[ Kosongkan Keranjang ]Diagram Alur Mini Project
useCartStore
│
┌────────────┼────────────┐
▼ ▼ ▼
State Getter Action
│ │ │
items totalItems addItem()
totalPrice removeItem()
│ clearCart()
▼
App.vue
│
▼
Tampilan BelanjaKunci: Pahami alur State → Getter (hitung) → Action (mutasi) → UI Update. Seluruh komponen yang membaca store akan otomatis terupdate secara sinkron.