Skip to content

Core Concepts

Pinia is built around four core concepts that work together to provide a powerful and intuitive state management solution. Understanding these concepts is essential for effectively using Pinia in your Vue.js applications.

What is a Store?

A store is a reactive entity that holds state, getters, and actions. Think of it as a component that can be used anywhere in your application. Stores are defined using the defineStore() function and can be used in any component, composable, or even other stores.

js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

Store Naming

The first argument to defineStore() is a unique identifier for the store. This ID is used by Pinia to connect the store to the devtools and for server-side rendering.

js
// ✅ Good naming conventions
const useUserStore = defineStore('user', { /* ... */ })
const useCartStore = defineStore('cart', { /* ... */ })
const useProductStore = defineStore('product', { /* ... */ })

// ❌ Avoid generic names
const useStore = defineStore('store', { /* ... */ })
const useDataStore = defineStore('data', { /* ... */ })

State

State is the central part of your store. It represents the reactive data that your application needs to manage. In Pinia, state is defined as a function that returns an object.

Defining State

js
export const useUserStore = defineStore('user', {
  state: () => ({
    // User information
    user: null,
    isAuthenticated: false,
    
    // UI state
    isLoading: false,
    error: null,
    
    // Application data
    preferences: {
      theme: 'light',
      language: 'en'
    },
    
    // Collections
    notifications: [],
    recentActivity: []
  })
})

Accessing State

State can be accessed directly from the store instance:

js
// In a component
const userStore = useUserStore()

// Direct access
console.log(userStore.user)
console.log(userStore.isAuthenticated)

// Reactive access in template
// <template>
//   <div v-if="userStore.isAuthenticated">
//     Welcome, {{ userStore.user.name }}!
//   </div>
// </template>

Mutating State

State can be mutated directly:

js
const userStore = useUserStore()

// Direct mutation
userStore.isLoading = true
userStore.user = { name: 'John', email: 'john@example.com' }

// Mutating nested objects
userStore.preferences.theme = 'dark'
userStore.notifications.push({ id: 1, message: 'Welcome!' })

Resetting State

You can reset the state to its initial value:

js
const userStore = useUserStore()

// Reset the entire store
userStore.$reset()

Getters

Getters are computed properties for your store. They allow you to derive state and cache the results. Getters receive the state as their first argument and can access other getters.

Basic Getters

js
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    tax: 0.1
  }),
  getters: {
    // Simple getter
    itemCount: (state) => state.items.length,
    
    // Getter with calculation
    subtotal: (state) => {
      return state.items.reduce((total, item) => {
        return total + (item.price * item.quantity)
      }, 0)
    },
    
    // Getter accessing other getters
    total() {
      return this.subtotal * (1 + this.tax)
    },
    
    // Getter with type annotation (TypeScript)
    expensiveItems: (state): CartItem[] => {
      return state.items.filter(item => item.price > 100)
    }
  }
})

Getters with Parameters

Getters can return functions to accept parameters:

js
export const useProductStore = defineStore('product', {
  state: () => ({
    products: []
  }),
  getters: {
    getProductById: (state) => {
      return (productId) => {
        return state.products.find(product => product.id === productId)
      }
    },
    
    getProductsByCategory: (state) => {
      return (category) => {
        return state.products.filter(product => product.category === category)
      }
    },
    
    searchProducts: (state) => {
      return (query) => {
        return state.products.filter(product => 
          product.name.toLowerCase().includes(query.toLowerCase())
        )
      }
    }
  }
})

// Usage
const productStore = useProductStore()
const product = productStore.getProductById('123')
const electronics = productStore.getProductsByCategory('electronics')
const searchResults = productStore.searchProducts('laptop')

Accessing Other Stores in Getters

js
export const useCartStore = defineStore('cart', {
  getters: {
    cartSummary() {
      const userStore = useUserStore()
      const productStore = useProductStore()
      
      return {
        items: this.items.map(item => ({
          ...item,
          product: productStore.getProductById(item.productId)
        })),
        user: userStore.user,
        total: this.total
      }
    }
  }
})

Actions

Actions are methods that can contain arbitrary logic, including asynchronous operations. They are the equivalent of methods in components and are where you should place business logic.

Synchronous Actions

js
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    },
    
    decrement() {
      this.count--
    },
    
    incrementBy(amount) {
      this.count += amount
    },
    
    reset() {
      this.count = 0
    }
  }
})

Asynchronous Actions

js
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    isLoading: false,
    error: null
  }),
  actions: {
    async fetchUser(userId) {
      this.isLoading = true
      this.error = null
      
      try {
        const response = await fetch(`/api/users/${userId}`)
        if (!response.ok) {
          throw new Error('Failed to fetch user')
        }
        this.user = await response.json()
      } catch (error) {
        this.error = error.message
      } finally {
        this.isLoading = false
      }
    },
    
    async updateUser(userData) {
      this.isLoading = true
      
      try {
        const response = await fetch(`/api/users/${this.user.id}`, {
          method: 'PUT',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(userData)
        })
        
        if (!response.ok) {
          throw new Error('Failed to update user')
        }
        
        this.user = await response.json()
      } catch (error) {
        this.error = error.message
        throw error // Re-throw to allow component to handle
      } finally {
        this.isLoading = false
      }
    }
  }
})

Actions Calling Other Actions

js
export const useAuthStore = defineStore('auth', {
  state: () => ({
    token: null,
    user: null
  }),
  actions: {
    async login(credentials) {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      })
      
      const data = await response.json()
      this.token = data.token
      
      // Call another action
      await this.fetchUserProfile()
    },
    
    async fetchUserProfile() {
      const response = await fetch('/api/profile', {
        headers: {
          'Authorization': `Bearer ${this.token}`
        }
      })
      
      this.user = await response.json()
    },
    
    logout() {
      this.token = null
      this.user = null
      
      // Call action from another store
      const cartStore = useCartStore()
      cartStore.clearCart()
    }
  }
})

Store Composition

Stores can use other stores, enabling powerful composition patterns:

js
// User store
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    preferences: {}
  }),
  actions: {
    async fetchUser(id) {
      // Fetch user logic
    }
  }
})

// Cart store that uses user store
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  getters: {
    cartWithUserInfo() {
      const userStore = useUserStore()
      return {
        items: this.items,
        user: userStore.user,
        appliedDiscounts: this.calculateDiscounts(userStore.user)
      }
    }
  },
  actions: {
    calculateDiscounts(user) {
      // Calculate discounts based on user data
      if (user?.isPremium) {
        return 0.1 // 10% discount for premium users
      }
      return 0
    },
    
    async checkout() {
      const userStore = useUserStore()
      
      if (!userStore.user) {
        throw new Error('User must be logged in to checkout')
      }
      
      // Checkout logic
    }
  }
})

Using Stores in Components

Options API

js
import { mapState, mapActions } from 'pinia'
import { useCounterStore } from '@/stores/counter'

export default {
  computed: {
    ...mapState(useCounterStore, ['count', 'doubleCount'])
  },
  methods: {
    ...mapActions(useCounterStore, ['increment', 'incrementBy'])
  }
}

Composition API

js
import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const counterStore = useCounterStore()
    
    return {
      // Direct access
      counterStore,
      
      // Destructured (loses reactivity)
      // count: counterStore.count,
      
      // Use storeToRefs for reactive destructuring
      ...storeToRefs(counterStore)
    }
  }
}

Script Setup

vue
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)
const { increment, incrementBy } = counterStore
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">+1</button>
    <button @click="incrementBy(5)">+5</button>
  </div>
</template>

Best Practices

1. Keep Stores Focused

Each store should have a single responsibility:

js
// ✅ Good - focused stores
const useUserStore = defineStore('user', { /* user-related state */ })
const useCartStore = defineStore('cart', { /* cart-related state */ })
const useProductStore = defineStore('product', { /* product-related state */ })

// ❌ Avoid - monolithic store
const useAppStore = defineStore('app', {
  state: () => ({
    user: {},
    cart: {},
    products: {},
    ui: {},
    // ... everything
  })
})

2. Use Actions for Business Logic

js
// ✅ Good - business logic in actions
actions: {
  async addToCart(product, quantity = 1) {
    // Validation
    if (quantity <= 0) {
      throw new Error('Quantity must be positive')
    }
    
    // Check inventory
    const productStore = useProductStore()
    if (!productStore.isInStock(product.id, quantity)) {
      throw new Error('Insufficient inventory')
    }
    
    // Add to cart
    const existingItem = this.items.find(item => item.id === product.id)
    if (existingItem) {
      existingItem.quantity += quantity
    } else {
      this.items.push({ ...product, quantity })
    }
    
    // Update inventory
    productStore.decreaseStock(product.id, quantity)
  }
}

// ❌ Avoid - business logic in components
// Component should just call the action

3. Use Getters for Computed Values

js
// ✅ Good - computed values as getters
getters: {
  totalPrice: (state) => {
    return state.items.reduce((total, item) => {
      return total + (item.price * item.quantity)
    }, 0)
  },
  
  formattedTotal() {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD'
    }).format(this.totalPrice)
  }
}

// ❌ Avoid - computing in components repeatedly

4. Handle Errors Gracefully

js
actions: {
  async fetchData() {
    this.isLoading = true
    this.error = null
    
    try {
      const data = await api.fetchData()
      this.data = data
    } catch (error) {
      this.error = error.message
      console.error('Failed to fetch data:', error)
    } finally {
      this.isLoading = false
    }
  }
}

Next Steps

Now that you understand the core concepts, explore:

Released under the MIT License.