Learn Vue - Modern State Management
Series/Learn Vue/Episode 10
Episode 10 of 24

Learn Vue - Modern State Management

This episode covers Pinia, Vue's official state manager: defining stores with state, getters, and actions, Pinia plugins for extension, the difference between local state and global stores, plus best practices for data normalization and modular stores in large apps.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

As an application grows, some state is needed in many distant components — for example, user data used in the header, the profile page, and the menu. Passing it through props becomes a mess; storing it in the root component is also fragile. The answer: global state management through a store.

Episode 10 covers Pinia, Vue's official state manager born from the lessons of Vuex. We'll dissect the store concept with state, getters, and actions, Pinia plugins for extending capabilities, the split between local state and global stores, and best practices for normalizing and modularizing stores in large applications.

Pinia as the Official State Manager

Install and Setup

Install Pinia and register it as a plugin:

Install Pinia
npm install pinia
JSSetup Pinia
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
 
const app = createApp(App);
app.use(createPinia());
app.mount("#app");

createPinia() creates the Pinia instance registered through app.use. After that, stores can be created and used in any component.

Writing Your First Store

Pinia uses a pattern similar to composables. A store is defined with defineStore:

JSStore keranjang
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", ...) creates a store with the unique name keranjang. The setup store uses the Composition API: items as state, totalHarga as a getter, and tambah as an action.

State, Getters, and Actions

State: Central Data

State is reactive data owned globally by the store. Unlike local state, store state can be accessed by any component without prop drilling.

Getters: Derived Values

Getters compute values from state, equivalent to computed:

JSGetter tambahan
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 is a pure getter, while itemById(id) is a parameterized getter — both use computed or plain functions to derive data.

Actions: Mutation Logic

Actions are where all state mutations and async logic live:

JSAction async
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() loads data from the API and updates the state. Routing all state changes through actions keeps the data flow traceable in one place.

Using a Store in a Component

JSMemakai store di komponen
<script setup>
import { useKeranjangStore } from "../stores/keranjang";
const store = useKeranjangStore();
</script>
 
<template>
  <p>Total item: {{ store.jumlahItem }}</p>
</template>

useKeranjangStore() gets the store instance; access state via store.xxx and call actions via store.tambah(...). Stores are reactive, so the UI updates along with them.

Local State vs Global Store

When to Use Which

The most important rule in state management: not all state needs to be global.

  • Local state: data used by only one component — open/close a modal, form values, the active tab. Keep it in a plain ref.
  • Global store: data used by many components or kept across pages — user session, cart, preferences.

Storing too much in the store bloats it and makes it hard to test. Start with local state, and move things to a store only when they truly need to be shared.

Best Practices for Large Stores

State Normalization

When data is relational, store it in normalized form to avoid duplication:

JSState ternormalisasi
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 maps ids to objects, semuaId holds the order. The map byId.value = Object.fromEntries(...) avoids data duplication — a technique you'll use for data that updates often.

Modular Stores

Split stores by domain instead of one giant store:

  • src/stores/user.js for the user domain
  • src/stores/keranjang.js for the cart
  • src/stores/produk.js for products
  • src/stores/notifikasi.js for notifications

Each store focuses on one domain and can use each other through useOtherStore().

Pinia Plugins

Plugins extend all stores at once, for example for persistence:

JSPlugin Pinia sederhana
import { piniaPluginPersist } from "./plugins/persist";
const pinia = createPinia();
pinia.use(piniaPluginPersist);

pinia.use(plugin) registers a plugin that wraps the store lifecycle. This pattern is used for localStorage persistence, action logging, or multi-tab synchronization — we'll expand on it in episode 14.

Summary

Episode 10 taught you modern state management with Pinia: defining stores with the setup syntax using state, getters, and actions, plugins for extension, clear rules for when state is local and when it's global, plus normalization and modularization for large applications.

Key takeaways:

  • Pinia is the official Vue 3 state manager.
  • Stores are built with defineStore and the setup syntax.
  • State for data, getters for derived values, actions for mutations.
  • Normalize state with byId to avoid duplication.
  • Split stores per domain and use plugins for cross-store needs.

In the next episode 11, we'll cover forms and validation — form handling with v-model, validation patterns with VeeValidate or Vuelidate, dynamic forms with custom validation rules, plus form accessibility and user feedback.

Learn Vue - Modern State Management | Learn Vue