intermediate Step 10 of 15

State Management with Pinia

Vue.js Development

State Management with Pinia

Pinia is the official state management library for Vue, replacing Vuex as the recommended solution. It provides a simple, type-safe way to share state across components without prop drilling or event buses. Pinia stores are defined with a clean API that feels like writing a regular Vue component, with state (like data), getters (like computed), and actions (like methods). Pinia integrates with Vue DevTools, supports hot module replacement, and has first-class TypeScript support. It is designed for Vue 3 but also works with Vue 2.

Setting Up Pinia

Install Pinia and register it as a Vue plugin. Then define stores in separate files.

// Install: npm install pinia

// main.js
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

const app = createApp(App);
app.use(createPinia());
app.mount("#app");

Defining a Store

Stores are defined using defineStore. You can use either the Options Store syntax (familiar to Options API users) or the Setup Store syntax (similar to Composition API).

// stores/counter.js — Options Store syntax
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  state: () => ({
    count: 0,
    lastIncrement: null,
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
    isPositive: (state) => state.count > 0,
  },
  actions: {
    increment() {
      this.count++;
      this.lastIncrement = new Date();
    },
    decrement() {
      this.count--;
    },
    async incrementAsync() {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      this.increment();
    },
  },
});

// stores/user.js — full example
export const useUserStore = defineStore("user", {
  state: () => ({
    currentUser: null,
    isAuthenticated: false,
    token: null,
  }),
  getters: {
    userName: (state) => state.currentUser?.name || "Guest",
    userRole: (state) => state.currentUser?.role || "viewer",
    isAdmin: (state) => state.currentUser?.role === "admin",
  },
  actions: {
    async login(email, password) {
      try {
        const response = await fetch("/api/login", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ email, password }),
        });
        const data = await response.json();
        this.currentUser = data.user;
        this.token = data.token;
        this.isAuthenticated = true;
        localStorage.setItem("token", data.token);
      } catch (error) {
        console.error("Login failed:", error);
        throw error;
      }
    },
    logout() {
      this.currentUser = null;
      this.token = null;
      this.isAuthenticated = false;
      localStorage.removeItem("token");
    },
  },
});

Using Stores in Components

Call the store's composable function inside your component to access state, getters, and actions. Use storeToRefs to destructure reactive state properties without losing reactivity.

<template>
  <div>
    <h2>Welcome, {{ userStore.userName }}</h2>
    <p>Count: {{ counterStore.count }}</p>
    <p>Double: {{ counterStore.doubleCount }}</p>
    <button @click="counterStore.increment()">+1</button>
    <button @click="counterStore.decrement()">-1</button>
    <button @click="counterStore.incrementAsync()">+1 (async)</button>
    <button @click="handleLogout">Logout</button>
  </div>
</template>

<script>
import { useCounterStore } from "../stores/counter";
import { useUserStore } from "../stores/user";

export default {
  setup() {
    const counterStore = useCounterStore();
    const userStore = useUserStore();
    return { counterStore, userStore };
  },
  methods: {
    handleLogout() {
      this.userStore.logout();
      this.$router.push("/login");
    },
  },
};
</script>
Tip: Keep stores focused on a single domain. Create separate stores for auth, cart, notifications, and other concerns rather than putting everything in one giant store. Stores can import and use other stores via their composable functions.

Key Takeaways

  • Pinia is Vue's official state management library, replacing Vuex with a simpler API.
  • Stores have state (reactive data), getters (computed values), and actions (methods).
  • Use defineStore with Options or Setup syntax to create stores.
  • Actions can be async and directly mutate state without the ceremony of mutations.
  • Keep stores focused on a single domain and compose them by importing one store into another.