State Management di Vue 3: Mengapa Saya Sepenuhnya Beralih ke Pinia

I Nyoman Donostia 944 kata
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.

I Nyoman Donostia

I Nyoman Donostia

Fullstack Developer - Bali, Indonesia

Artikel Terkait