Episode ini membahas Pinia, state manager resmi Vue: definisi store dengan state, getters, dan actions, plugin Pinia untuk ekstensi, perbedaan local state dan global store, serta best practices untuk normalisasi data dan modular stores di aplikasi besar.

Saat aplikasi tumbuh, beberapa state dibutuhkan di banyak komponen yang berjauhan — misalnya data user yang dipakai di header, halaman profil, dan menu. Mengoper lewat props akan berantakan; menyimpan di komponen root juga rapuh. Jawabannya: state management global lewat sebuah store.
Episode 10 membahas Pinia, state manager resmi Vue yang lahir dari pengalaman Vuex. Kita bedah konsep store dengan state, getters, dan actions, plugin Pinia untuk memperluas kemampuan, pembagian local state vs global store, dan best practices untuk normalisasi serta modularisasi store di aplikasi besar.
Pasang Pinia dan daftarkan sebagai plugin:
npm install piniaimport { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
const app = createApp(App);
app.use(createPinia());
app.mount("#app");createPinia() membuat instance Pinia yang didaftarkan lewat app.use. Setelah itu, store bisa dibuat dan dipakai di komponen mana pun.
Pinia menggunakan pattern yang mirip composable. Store didefinisikan dengan defineStore:
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useKeranjangStore = defineStore("keranjang", () => {
const items = ref([]);
const totalHarga = computed(() =>
items.value.reduce((total, item) => total + item.harga, 0)
);
function tambah(item) {
items.value.push(item);
}
return { items, totalHarga, tambah };
});defineStore("keranjang", ...) membuat store dengan nama unik keranjang. Setup store memakai composition API: items sebagai state, totalHarga sebagai getter, dan tambah sebagai action.
State adalah data reaktif yang dimiliki store secara global. Beda dengan local state, state store bisa diakses komponen mana pun tanpa prop drilling.
Getters menghitung nilai dari state, setara computed:
export const useKeranjangStore = defineStore("keranjang", () => {
const items = ref([]);
const jumlahItem = computed(() => items.value.length);
function itemById(id) {
return items.value.find((item) => item.id === id);
}
return { items, jumlahItem, itemById };
});jumlahItem adalah getter murni, sedangkan itemById(id) adalah getter berparameter — keduanya memakai computed atau fungsi biasa untuk menurunkan data.
Actions adalah tempat seluruh mutasi state dan logika async berada:
export const useKeranjangStore = defineStore("keranjang", () => {
const items = ref([]);
async function muatDariServer() {
const res = await fetch("/api/keranjang");
items.value = await res.json();
}
return { items, muatDariServer };
});muatDariServer() memuat data dari API dan memperbarui state. Seluruh perubahan state melalui actions membuat alur data dapat dilacak di satu tempat.
<script setup>
import { useKeranjangStore } from "../stores/keranjang";
const store = useKeranjangStore();
</script>
<template>
<p>Total item: {{ store.jumlahItem }}</p>
</template>useKeranjangStore() mengambil instance store; akses state lewat store.xxx dan panggil action lewat store.tambah(...). Store bersifat reactive sehingga UI ikut diperbarui.
Rule paling penting dalam state management: tidak semua state perlu global.
ref biasa.Menyimpan terlalu banyak ke store membuat store membengkak dan sulit diuji. Mulailah dari local state, dan pindahkan ke store hanya ketika benar-benar dibutuhkan bersama.
Saat data berelasi, simpan dalam bentuk normal agar tidak duplikat:
export const useProdukStore = defineStore("produk", () => {
const byId = ref({});
const semuaId = ref([]);
function setProduk(list) {
byId.value = Object.fromEntries(list.map((p) => [p.id, p]));
semuaId.value = list.map((p) => p.id);
}
return { byId, semuaId, setProduk };
});byId memetakan id ke objek, semuaId menyimpan urutan. Peta byId.value = Object.fromEntries(...) menghindari duplikasi data — teknik yang akan kalian pakai untuk data yang di-update sering.
Bagi store per domain, bukan satu store raksasa:
src/stores/user.js untuk domain usersrc/stores/keranjang.js untuk keranjangsrc/stores/produk.js untuk produksrc/stores/notifikasi.js untuk notifikasiSetiap store fokus pada satu domain dan bisa saling memakai lewat useOtherStore().
Plugin memperluas semua store sekaligus, misalnya untuk persistence:
import { piniaPluginPersist } from "./plugins/persist";
const pinia = createPinia();
pinia.use(piniaPluginPersist);pinia.use(plugin) meregistrasi plugin yang membungkus lifecycle store. Pattern ini digunakan untuk persistence ke localStorage, logging aksi, atau sinkronisasi multi-tab — akan kita perluas di episode 14.
Episode 10 mengajarkan state management modern dengan Pinia: definisi store memakai setup syntax dengan state, getters, dan actions, plugin untuk ekstensi, aturan jelas kapan state lokal dan kapan global, serta normalisasi dan modularisasi untuk aplikasi besar.
Inti yang harus dibawa pulang:
defineStore dan setup syntax.byId untuk menghindari duplikasi.Di episode 11 selanjutnya kita membahas forms dan validation — penanganan form dengan v-model, pattern validasi dengan VeeValidate atau Vuelidate, dynamic forms dengan custom validation rules, serta accessibility dan umpan balik pengguna pada form.