Back to list
mgd34msu

pinia

by mgd34msu

Plug in, Receive good vibes.

2🍴 0📅 Jan 25, 2026

SKILL.md


name: pinia description: Manages Vue state with Pinia including stores, getters, actions, and plugins. Use when building Vue applications needing centralized state, sharing state between components, or replacing Vuex.

Pinia

The intuitive, type safe, and flexible store for Vue.

Quick Start

Install:

npm install pinia

Setup (main.ts):

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

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

Defining Stores

Option Store (Vue Options API style)

// stores/counter.ts
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter',
  }),

  getters: {
    doubleCount: (state) => state.count * 2,

    // Getter using other getters
    doubleCountPlusOne(): number {
      return this.doubleCount + 1;
    },
  },

  actions: {
    increment() {
      this.count++;
    },

    async fetchAndSet() {
      const response = await fetch('/api/count');
      const data = await response.json();
      this.count = data.count;
    },
  },
});

Setup Store (Composition API style)

// stores/counter.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  // State
  const count = ref(0);
  const name = ref('Counter');

  // Getters
  const doubleCount = computed(() => count.value * 2);

  // Actions
  function increment() {
    count.value++;
  }

  async function fetchAndSet() {
    const response = await fetch('/api/count');
    const data = await response.json();
    count.value = data.count;
  }

  return { count, name, doubleCount, increment, fetchAndSet };
});

Using Stores

Basic Usage

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';

const counter = useCounterStore();

// Access state
console.log(counter.count);

// Access getters
console.log(counter.doubleCount);

// Call actions
counter.increment();
</script>

<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <p>Double: {{ counter.doubleCount }}</p>
    <button @click="counter.increment">Increment</button>
  </div>
</template>

Destructuring with storeToRefs

<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useCounterStore } from '@/stores/counter';

const counter = useCounterStore();

// Destructure with reactivity preserved
const { count, doubleCount } = storeToRefs(counter);

// Actions can be destructured directly
const { increment } = counter;
</script>

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

State

Accessing State

const store = useCounterStore();

// Direct access
store.count;

// Via $state
store.$state.count;

Modifying State

const store = useCounterStore();

// Direct mutation
store.count++;

// Patch single property
store.$patch({ count: 10 });

// Patch multiple properties
store.$patch({
  count: 10,
  name: 'New Counter',
});

// Patch with function
store.$patch((state) => {
  state.count++;
  state.items.push({ id: 1 });
});

// Replace entire state
store.$state = { count: 0, name: 'Reset' };

// Reset to initial state
store.$reset();

Getters

Basic Getters

export const useProductStore = defineStore('products', {
  state: () => ({
    items: [] as Product[],
  }),

  getters: {
    // Arrow function
    itemCount: (state) => state.items.length,

    // Using this for other getters
    hasItems(): boolean {
      return this.itemCount > 0;
    },

    // Getter with parameter (returns function)
    getById: (state) => {
      return (id: string) => state.items.find(item => item.id === id);
    },
  },
});

Using Other Store Getters

import { useUserStore } from './user';

export const useCartStore = defineStore('cart', {
  getters: {
    userCart(): CartItem[] {
      const userStore = useUserStore();
      return this.items.filter(item => item.userId === userStore.currentUserId);
    },
  },
});

Actions

Basic Actions

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as User | null,
    token: null as string | null,
  }),

  actions: {
    async login(email: string, password: string) {
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify({ email, password }),
        });

        const data = await response.json();
        this.user = data.user;
        this.token = data.token;

        return data;
      } catch (error) {
        this.user = null;
        this.token = null;
        throw error;
      }
    },

    logout() {
      this.user = null;
      this.token = null;
      this.$reset();
    },
  },
});

Using Other Stores in Actions

import { useNotificationStore } from './notification';

export const useCartStore = defineStore('cart', {
  actions: {
    async checkout() {
      const notificationStore = useNotificationStore();

      try {
        await this.submitOrder();
        notificationStore.show('Order placed!');
      } catch (error) {
        notificationStore.show('Order failed', 'error');
      }
    },
  },
});

Subscribing to Changes

State Subscription

const store = useCounterStore();

// Subscribe to state changes
store.$subscribe((mutation, state) => {
  console.log('State changed:', mutation.type);
  console.log('New state:', state);

  // Persist to localStorage
  localStorage.setItem('counter', JSON.stringify(state));
});

// With options
store.$subscribe(
  (mutation, state) => {
    // ...
  },
  { detached: true } // Survives component unmount
);

Action Subscription

const store = useAuthStore();

// Subscribe to actions
store.$onAction(({ name, args, after, onError }) => {
  console.log(`Action ${name} called with:`, args);

  after((result) => {
    console.log(`Action ${name} finished with:`, result);
  });

  onError((error) => {
    console.error(`Action ${name} failed:`, error);
  });
});

Plugins

Creating a Plugin

// plugins/persistedState.ts
import { PiniaPluginContext } from 'pinia';

export function piniaPersistedState({ store }: PiniaPluginContext) {
  // Restore state from localStorage
  const savedState = localStorage.getItem(store.$id);
  if (savedState) {
    store.$patch(JSON.parse(savedState));
  }

  // Subscribe to changes
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state));
  });
}

// main.ts
const pinia = createPinia();
pinia.use(piniaPersistedState);

Adding Properties to Stores

import { markRaw } from 'vue';
import { Router } from 'vue-router';

declare module 'pinia' {
  export interface PiniaCustomProperties {
    router: Router;
  }
}

const pinia = createPinia();
pinia.use(({ store }) => {
  store.router = markRaw(router);
});

TypeScript

Typed Store

interface UserState {
  user: User | null;
  isLoading: boolean;
  error: string | null;
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    user: null,
    isLoading: false,
    error: null,
  }),

  getters: {
    isLoggedIn: (state): boolean => !!state.user,
    fullName(): string {
      return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';
    },
  },

  actions: {
    async fetchUser(id: string): Promise<void> {
      this.isLoading = true;
      try {
        const response = await fetch(`/api/users/${id}`);
        this.user = await response.json();
      } catch (e) {
        this.error = (e as Error).message;
      } finally {
        this.isLoading = false;
      }
    },
  },
});

Testing

import { setActivePinia, createPinia } from 'pinia';
import { useCounterStore } from '@/stores/counter';
import { describe, it, expect, beforeEach } from 'vitest';

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
  });

  it('increments count', () => {
    const counter = useCounterStore();
    expect(counter.count).toBe(0);

    counter.increment();
    expect(counter.count).toBe(1);
  });

  it('computes double count', () => {
    const counter = useCounterStore();
    counter.count = 5;
    expect(counter.doubleCount).toBe(10);
  });
});

Composing Stores

// stores/cart.ts
import { useProductStore } from './products';
import { useUserStore } from './user';

export const useCartStore = defineStore('cart', () => {
  const productStore = useProductStore();
  const userStore = useUserStore();

  const items = ref<CartItem[]>([]);

  const total = computed(() => {
    return items.value.reduce((sum, item) => {
      const product = productStore.getById(item.productId);
      return sum + (product?.price ?? 0) * item.quantity;
    }, 0);
  });

  const discountedTotal = computed(() => {
    const discount = userStore.user?.discount ?? 0;
    return total.value * (1 - discount);
  });

  return { items, total, discountedTotal };
});

Best Practices

  1. One store per domain - User store, cart store, etc.
  2. Use Setup Stores for complex logic - Better composition
  3. Use storeToRefs for destructuring - Maintains reactivity
  4. Keep actions async-aware - Return promises
  5. Use plugins for cross-cutting concerns - Persistence, logging

Common Mistakes

MistakeFix
Destructuring state directlyUse storeToRefs()
Calling useStore outside setupCall inside setup or actions
Mutating state in gettersKeep getters pure
Circular store dependenciesRefactor to avoid cycles
Not using $resetUse it to reset to initial

Reference Files

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon