State Management di Vue 3: Mengapa Saya Sepenuhnya Beralih ke Pinia
Sejarah Singkat: Vuex dan Evolusinya
Vuex adalah state management resmi Vue.js selama bertahun-tahun. Saya memakainya di proyek Vue 2 production dengan ~30 komponen. Pengalaman? Bekerja, tapi penuh boilerplate. Mutations, actions, getters, modules — terlalu banyak concept untuk satu hal.
Lalu Vue 3 dan Composition API datang. Evan You dan tim menyadari Vuex perlu evolusi. Lahir Pinia — yang secara resmi menjadi state management rekomendasi untuk Vue 3.
Artikel ini menjelaskan kenapa saya tidak menyesal pindah.
Perbandingan Struktural
Vuex Store: Boilerplate Berat
// store/user.js (Vuex)
const state = () => ({
user: null,
loading: false,
error: null,
});
const getters = {
isAuthenticated: state => !!state.user,
fullName: state => state.user ? `${state.user.firstName} ${state.user.lastName}` : '',
};
const mutations = {
SET_USER(state, user) {
state.user = user;
},
SET_LOADING(state, value) {
state.loading = value;
},
SET_ERROR(state, error) {
state.error = error;
},
};
const actions = {
async login({ commit }, { email, password }) {
commit('SET_LOADING', true);
try {
const user = await api.login(email, password);
commit('SET_USER', user);
localStorage.setItem('token', user.token);
} catch (err) {
commit('SET_ERROR', err.message);
throw err;
} finally {
commit('SET_LOADING', false);
}
}
};
export default {
namespaced: true,
state,
getters,
mutations,
actions,
};
Pinia Store: Lebih Ringkas
// stores/user.ts (Pinia)
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
loading: false,
error: null as string | null,
}),
getters: {
isAuthenticated: state => !!state.user,
fullName: state => state.user ? `${state.user.firstName} ${state.user.lastName}` : '',
},
actions: {
async login(email: string, password: string) {
this.loading = true;
try {
this.user = await api.login(email, password);
localStorage.setItem('token', this.user.token);
} catch (err: any) {
this.error = err.message;
throw err;
} finally {
this.loading = false;
}
}
}
});
Pinia:
- ❌ Tidak ada mutations (langsung mutate state di actions)
- ✅ Akses state via
this(bukan context destructure) - ✅ Nama store bebas (no namespace manual)
- ✅ TypeScript infer state otomatis
Setup Store (Composition API Style)
Pinia offer setup stores — sintaks yang 100% identik dengan Composition API Vue.
// stores/user.ts (Setup Style)
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const isAuthenticated = computed(() => !!user.value);
const fullName = computed(() => user.value ? `${user.value.firstName} ${user.value.lastName}` : '');
async function login(email: string, password: string) {
loading.value = true;
try {
user.value = await api.login(email, password);
localStorage.setItem('token', user.value.token);
} catch (err: any) {
error.value = err.message;
throw err;
} finally {
loading.value = false;
}
}
function logout() {
user.value = null;
localStorage.removeItem('token');
}
return { user, loading, error, isAuthenticated, fullName, login, logout };
});
Penggunaan di Komponen
<template>
<div>
<p v-if="loading">Loading...</p>
<p v-else-if="error" class="error">{{ error }}</p>
<div v-else>
<h1>Welcome, {{ fullName }}</h1>
<button @click="handleLogout">Logout</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
// Store values reactif, gunakan storeToRefs untuk disconnect dari reactive proxy
const { user, loading, error, isAuthenticated, fullName } = storeToRefs(userStore);
const handleLogout = () => userStore.logout();
</script>
Mengapa Saya Pindah Total?
1. Mutations yang Hilang
Vuex memaksa setiap perubahan state lewat mutation. Reason: devtools tracking. Tapi reality: 80% mutations adalah setter trivial (SET_USER, SET_LOADING). Pinia hapus konsep ini — langsung mutate state di action.
2. Type Safety yang Sebenarnya
Vuex dengan TypeScript menyakitkan. GetterTree, ActionTree, manual typing untuk context di actions.
Pinia: type inference otomatis dari state() return, getter otomatis typed oleh computed.
// Type tersedia otomatis
const store = useUserStore();
store.user // typed sebagai User | null
store.fullName // typed sebagai string
3. Kode Lebih Modular
Vuex modules butuh konfigurasi namespaced: true, import manual, dan namespace string di dispatch.`
Pinia modules: cukup buat beberapa file defineStore('name') — import langsung. Auto-namespaced.
stores/
├── user.ts // useUserStore
├── cart.ts // useCartStore
├── products.ts // useProductsStore
└── auth.ts // useAuthStore
// Tidak perlu register module global
import { useUserStore } from '@/stores/user';
import { useCartStore } from '@/stores/cart';
const user = useUserStore();
const cart = useCartStore();```
### 4. Store Composition
Pinia store bisa store composition (interaksi antar store:
```ts
// stores/cart.ts
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', {
actions: {
async checkout() {
const userStore = useUserStore();
if (!userStore.isAuthenticated) throw new Error('Login required');
// ... proceed order
}
}
});
5. DevTools Support
Pinia terintegrasi sempurna dengan Vue DevTools: timeline mutation, time travel debugging, dan state inspector otomatis.
Fitur Advanced Pinia
Subscriptions
const store = useUserStore();
store.$subscribe((mutation, state) => {
// Persist ke localStorage ketika state berubah
localStorage.setItem('user', JSON.stringify(state.user));
});
store.$onAction(({ after, onError }) => {
after(result => console.log('Action succeeded'));
onError(error => console.error('Action failed:', error));
});
Reset State
// Vuex: butuh custom action
// Pinia: tinggal panggil method builtin
const store = useUserStore();
store.$reset();
Plugins
// plugins/persist.ts
import type { PiniaPluginContext } from 'pinia';
export function persistPlugin({ store, options }: PiniaPluginContext) {
if (options.persist) {
const savedState = localStorage.getItem(store.$id);
if (savedState) store.$patch(JSON.parse(savedState));
store.$subscribe((_, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
}
}
// main.ts
const pinia = createPinia();
pinia.use(persistPlugin);
Server-side rendering (SSR) dengan hydration state juga straightforward di Pinia.
Kesimpulan
Untuk proyek Vue 3 baru, tidak ada alasan pakai Vuex. Pinia adalah pilihan resmi, lebih ringkas, type-safe, dan dev experience jauh lebih baik. Migrasi dari Vuex ke Pinia pun bi.s dilakukan bertahap store-per-store dengan adapter.
Saya pindah semua proyek Vue 3 saya ke Pinia dalam 2 hari. Tidak menyesal.