Using Pinia with Options API
While Pinia is designed with the Composition API in mind, it also provides excellent support for the Options API through a set of helper functions. This guide covers how to use Pinia stores effectively within Vue's Options API.
Overview
Pinia provides several helper functions to integrate stores with the Options API:
mapState()
- Map store state to computed propertiesmapWritableState()
- Map store state to writable computed propertiesmapActions()
- Map store actions to component methodsmapStores()
- Map entire stores to component properties
Basic Store Setup
First, let's define a basic store:
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter'
}),
getters: {
doubleCount: (state) => state.count * 2,
isEven: (state) => state.count % 2 === 0
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
setCount(value) {
this.count = value
}
}
})
Using mapState
The mapState()
helper maps store state and getters to computed properties:
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
<p>Is Even: {{ isEven }}</p>
<p>Store Name: {{ name }}</p>
</div>
</template>
<script>
import { mapState } from 'pinia'
import { useCounterStore } from '@/stores/counter'
export default {
computed: {
// Map state and getters
...mapState(useCounterStore, ['count', 'doubleCount', 'isEven', 'name'])
}
}
</script>
Custom Property Names
You can also map to custom property names:
export default {
computed: {
...mapState(useCounterStore, {
myCount: 'count',
myName: 'name',
double: 'doubleCount'
})
}
}
Using mapWritableState
For state that needs to be writable, use mapWritableState()
:
<template>
<div>
<input v-model="count" type="number" />
<input v-model="name" type="text" />
</div>
</template>
<script>
import { mapWritableState } from 'pinia'
import { useCounterStore } from '@/stores/counter'
export default {
computed: {
// These create writable computed properties
...mapWritableState(useCounterStore, ['count', 'name'])
}
}
</script>
WARNING
mapWritableState()
cannot be used with getters since they are read-only.
Using mapActions
The mapActions()
helper maps store actions to component methods:
<template>
<div>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="setCount(10)">Set to 10</button>
</div>
</template>
<script>
import { mapActions } from 'pinia'
import { useCounterStore } from '@/stores/counter'
export default {
methods: {
...mapActions(useCounterStore, ['increment', 'decrement', 'setCount'])
}
}
</script>
Custom Method Names
export default {
methods: {
...mapActions(useCounterStore, {
add: 'increment',
subtract: 'decrement',
updateCount: 'setCount'
})
}
}
Using mapStores
The mapStores()
helper gives you access to entire stores:
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<button @click="counterStore.increment()">Increment</button>
</div>
</template>
<script>
import { mapStores } from 'pinia'
import { useCounterStore } from '@/stores/counter'
import { useUserStore } from '@/stores/user'
export default {
computed: {
...mapStores(useCounterStore, useUserStore)
// Creates counterStore and userStore properties
}
}
</script>
Combining Multiple Helpers
You can combine multiple helpers in the same component:
<template>
<div>
<!-- Read-only state -->
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<!-- Writable state -->
<input v-model="name" />
<!-- Actions -->
<button @click="increment">+</button>
<button @click="decrement">-</button>
<!-- Direct store access -->
<button @click="counterStore.setCount(0)">Reset</button>
</div>
</template>
<script>
import { mapState, mapWritableState, mapActions, mapStores } from 'pinia'
import { useCounterStore } from '@/stores/counter'
export default {
computed: {
...mapState(useCounterStore, ['count', 'doubleCount']),
...mapWritableState(useCounterStore, ['name']),
...mapStores(useCounterStore)
},
methods: {
...mapActions(useCounterStore, ['increment', 'decrement'])
}
}
</script>
Working with Multiple Stores
<script>
import { mapState, mapActions } from 'pinia'
import { useCounterStore } from '@/stores/counter'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
export default {
computed: {
...mapState(useCounterStore, ['count']),
...mapState(useUserStore, ['user', 'isLoggedIn']),
...mapState(useCartStore, ['items', 'total'])
},
methods: {
...mapActions(useCounterStore, ['increment']),
...mapActions(useUserStore, ['login', 'logout']),
...mapActions(useCartStore, ['addItem', 'removeItem'])
}
}
</script>
Advanced Patterns
Conditional Mapping
You can conditionally map properties:
export default {
computed: {
...mapState(useCounterStore, ['count']),
// Conditionally map user store
...(this.showUserInfo ? mapState(useUserStore, ['user']) : {})
}
}
Custom Computed Properties
Combine mapped state with custom computed properties:
export default {
computed: {
...mapState(useCounterStore, ['count']),
// Custom computed property using mapped state
countMessage() {
return `Current count is ${this.count}`
},
// Computed property combining multiple stores
summary() {
return {
count: this.count,
user: this.user,
timestamp: Date.now()
}
}
}
}
Method Composition
Combine mapped actions with custom methods:
export default {
methods: {
...mapActions(useCounterStore, ['increment', 'setCount']),
// Custom method using mapped actions
incrementBy(amount) {
for (let i = 0; i < amount; i++) {
this.increment()
}
},
// Method with validation
safeSetCount(value) {
if (value >= 0 && value <= 100) {
this.setCount(value)
}
}
}
}
TypeScript Support
Pinia's Options API helpers work well with TypeScript:
import { defineComponent } from 'vue'
import { mapState, mapActions } from 'pinia'
import { useCounterStore } from '@/stores/counter'
export default defineComponent({
computed: {
...mapState(useCounterStore, ['count', 'doubleCount'])
},
methods: {
...mapActions(useCounterStore, ['increment', 'decrement'])
}
})
Best Practices
1. Organize Imports
// Group Pinia imports together
import { mapState, mapWritableState, mapActions, mapStores } from 'pinia'
// Group store imports
import { useCounterStore } from '@/stores/counter'
import { useUserStore } from '@/stores/user'
2. Use Descriptive Names
export default {
computed: {
// Use descriptive names for clarity
...mapState(useCounterStore, {
currentCount: 'count',
counterName: 'name'
})
}
}
3. Group Related Mappings
export default {
computed: {
// Counter store mappings
...mapState(useCounterStore, ['count', 'doubleCount']),
// User store mappings
...mapState(useUserStore, ['user', 'isLoggedIn'])
},
methods: {
// Counter actions
...mapActions(useCounterStore, ['increment', 'decrement']),
// User actions
...mapActions(useUserStore, ['login', 'logout'])
}
}
4. Avoid Overuse of mapStores
Use mapStores
sparingly, prefer specific mappings:
// Preferred: Specific mappings
export default {
computed: {
...mapState(useCounterStore, ['count']),
...mapActions(useCounterStore, ['increment'])
}
}
// Use mapStores only when you need full store access
export default {
computed: {
...mapStores(useCounterStore)
},
methods: {
complexOperation() {
// When you need to call multiple store methods
this.counterStore.increment()
this.counterStore.setCount(this.counterStore.count * 2)
}
}
}
Migration from Vuex
If you're migrating from Vuex, the mapping is straightforward:
// Vuex
export default {
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
},
methods: {
...mapActions(['increment'])
}
}
// Pinia
export default {
computed: {
...mapState(useCounterStore, ['count', 'doubleCount'])
},
methods: {
...mapActions(useCounterStore, ['increment'])
}
}