Frontend Development 15 min read

Comprehensive Guide to Using Pinia for State Management in Vue

This article provides an in‑depth tutorial on Pinia, covering its origins, differences from Vuex, installation, store definition, state, getters, actions, plugin creation, persistence strategies, and practical code examples for integrating Pinia into Vue applications.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Comprehensive Guide to Using Pinia for State Management in Vue

Introduction

Pinia (pronounced /piːnjʌ/) is a state‑management library for Vue.js, created by members of the Vue core team as the next iteration of Vuex. It offers a simpler API, Composition‑API style usage, and full TypeScript type inference.

Differences between Pinia and Vuex 3.x/4.x

Mutations are removed; only state , getters , and actions exist.

actions can modify state synchronously or asynchronously.

Built‑in TypeScript support with reliable type inference.

No module nesting – stores are independent and can call each other.

Plugin system enables easy extensions such as local storage.

Very lightweight – the compressed bundle is about 2 KB.

Basic Usage

Installation

Install Pinia via npm:

npm install pinia

Import and create Pinia in main.js :

// src/main.js
import { createPinia } from 'pinia'

const pinia = createPinia()
app.use(pinia)

Defining a Store

Create src/stores/counter.js and use defineStore() :

// src/stores/counter.js
import { defineStore } from 'pinia'

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

Alternatively, define the store with a setup function:

// src/stores/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }
  return { count, doubleCount, increment }
})

Using the Store in a Component

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

const counterStore = useCounterStore()
// All three mutations are tracked by devtools
counterStore.count++
counterStore.$patch({ count: counterStore.count + 1 })
counterStore.increment()
</script>

<template>
  <div>{{ counterStore.count }}</div>
  <div>{{ counterStore.doubleCount }}</div>
</template>

State

Destructuring a Store

Because a store is wrapped with reactive , direct destructuring loses reactivity. Use storeToRefs() instead:

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

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

<template>
  <div>{{ count }}</div>
  <div>{{ doubleCount }}</div>
</template>

Modifying State

You can modify state directly ( store.count++ ) or use the higher‑performance $patch method to batch updates:

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

const counterStore = useCounterStore()
counterStore.$patch({
  count: counterStore.count + 1,
  name: 'Abalam'
})
</script>

$patch also accepts a function for complex mutations:

cartStore.$patch(state => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

Listening to Store Changes

Use $subscribe() to react to any state change, similar to Vuex subscribe but with a single callback after multiple mutations:

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

const counterStore = useCounterStore()
counterStore.$subscribe((mutation, state) => {
  // Persist state to localStorage on every change
  localStorage.setItem('counter', JSON.stringify(state))
})
</script>

You can also watch all stores via the Pinia instance:

import { watch } from 'vue'
import { createPinia } from 'pinia'

const pinia = createPinia()
watch(
  pinia.state,
  state => {
    localStorage.setItem('piniaState', JSON.stringify(state))
  },
  { deep: true }
)

Getters

Accessing Store Instance

Getters can reference this to access other getters within the same store:

// src/stores/counter.js
import { defineStore } from 'pinia'

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

Using Getters from Another Store

// src/stores/counter.js
import { defineStore } from 'pinia'
import { useOtherStore } from './otherStore'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 1 }),
  getters: {
    composeGetter(state) {
      const otherStore = useOtherStore()
      return state.count + otherStore.count
    }
  }
})

Passing Parameters to a Getter

Return a function from a getter to accept arguments:

// src/stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    users: [
      { id: 1, name: 'Tom' },
      { id: 2, name: 'Jack' }
    ]
  }),
  getters: {
    getUserById: state => userId => state.users.find(user => user.id === userId)
  }
})

// Component usage
<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
const { getUserById } = storeToRefs(userStore)
</script>

<template>
  <p>User: {{ getUserById(2) }}</p>
</template>

Actions

Accessing Store Instance

Actions can use this and may be asynchronous:

// src/stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({ userData: null }),
  actions: {
    async registerUser(login, password) {
      try {
        this.userData = await api.post({ login, password })
      } catch (error) {
        return error
      }
    }
  }
})

Calling Actions of Another Store

// src/stores/setting.js
import { defineStore } from 'pinia'
import { useAuthStore } from './authStore'

export const useSettingStore = defineStore('setting', {
  state: () => ({ preferences: null }),
  actions: {
    async fetchUserPreferences(preferences) {
      const authStore = useAuthStore()
      if (authStore.isAuthenticated()) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated!')
      }
    }
  }
})

Plugins

Pinia’s plugin system allows you to extend stores at a low level. A plugin is a function that receives a context object containing app , pinia , store , and options . It can return an object whose properties are merged into the store’s state or actions.

export function myPiniaPlugin(context) {
  // context.app – Vue 3 app instance
  // context.pinia – Pinia instance
  // context.store – store being extended
  // context.options – options passed to defineStore()
  return {
    hello: 'world', // adds state property
    changeHello() { // adds action
      this.hello = 'pinia'
    }
  }
}

Register the plugin:

// src/main.js
import { createPinia } from 'pinia'
import { myPiniaPlugin } from './myPlugin'

const pinia = createPinia()
pinia.use(myPiniaPlugin)

Adding New State via Plugin

pinia.use(() => ({ hello: 'world' }))
import { ref, toRef } from 'vue'

pinia.use(({ store }) => {
  const hello = ref('word')
  store.$state.hello = hello
  store.hello = toRef(store.$state, 'hello')
})

Adding Custom Options

You can define custom options (e.g., debounce ) in a store and let a plugin act on them:

// src/stores/search.js
import { defineStore } from 'pinia'

export const useSearchStore = defineStore('search', {
  actions: {
    searchContacts() {},
    searchContent() {}
  },
  debounce: {
    searchContacts: 300,
    searchContent: 500
  }
})
// src/main.js
import { createPinia } from 'pinia'
import { debounce } from 'lodash'

const pinia = createPinia()
pinia.use(({ options, store }) => {
  if (options.debounce) {
    return Object.keys(options.debounce).reduce((acc, action) => {
      acc[action] = debounce(store[action], options.debounce[action])
      return acc
    }, {})
  }
})

Persisting State

Install the persistence plugin and enable it in a store:

npm i pinia-plugin-persist
// src/main.js
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'

const pinia = createPinia()
pinia.use(piniaPluginPersist)
// src/stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 1 }),
  persist: { enabled: true }
})

You can customize the storage key, target storage, and which state paths to persist:

persist: {
  enabled: true,
  strategies: [
    {
      key: 'myCounter',
      storage: localStorage,
      paths: ['name', 'age']
    }
  ]
}

Conclusion

Pinia is lighter and simpler than Vuex while offering richer features, TypeScript support, and a powerful plugin system that can handle tasks such as debouncing actions and persisting state. It can also be used in Vue 2 via the map helpers.

frontendTypeScriptJavaScriptState ManagementVuePiniaplugins
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.