Reactivity Patterns
13.3.1. storeToRefs
When destructuring state and getters from a Store, storeToRefs() must always be used. Using plain destructuring will cause loss of reactivity.
- state, getters: Must be destructured with
storeToRefs(). - actions: Must be destructured directly. Actions are plain functions, so
storeToRefs()is unnecessary.
typescript
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/useUserStore'
const userStore = useUserStore()
// Extract state and getters with storeToRefs.
const { user, isLoggedIn, displayName } = storeToRefs(userStore)
// Destructure actions directly.
const { loadCurrentUser, logout } = userStoreThe following is an incorrect pattern. Reactivity is lost, and the template will not update.
typescript
// Prohibited: reactivity is lost.
const { user, isLoggedIn } = useUserStore()13.3.2. Using computed
Derived state within a Store must be defined with computed(). The same calculation must not be repeated across components.
typescript
// src/stores/useCartStore.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import type { CartItem } from '@/types/cart'
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
// Derived state must be defined with computed.
const itemCount = computed(() => items.value.length)
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const isEmpty = computed(() => items.value.length === 0)
return { items, itemCount, totalPrice, isEmpty }
})- When the same derived logic is used across multiple components, it must be defined as a Store getter rather than in individual components.
- When consuming Store getters in a component,
storeToRefs()must be used.
typescript
// Consuming getters in a component
const cartStore = useCartStore()
const { totalPrice, isEmpty } = storeToRefs(cartStore)13.3.3. $subscribe
$subscribe() may be used to detect Store state changes.
typescript
const userStore = useUserStore()
userStore.$subscribe((mutation, state) => {
// mutation.type: 'direct' | 'patch object' | 'patch function'
// mutation.storeId: Store ID
// state: state after the change
localStorage.setItem('user', JSON.stringify(state.user))
})The options for $subscribe() are as follows.
| Option | Type | Description |
|---|---|---|
detached | boolean | When set to true, the subscription persists after the component is unmounted. |
deep | boolean | Detects changes in nested objects. Defaults to true. |
flush | 'pre' | 'post' | 'sync' | Specifies the callback execution timing. |
typescript
// Pattern to persist subscription after component unmount
userStore.$subscribe(
(mutation, state) => {
sessionStorage.setItem('user-state', JSON.stringify(state))
},
{ detached: true }
)detached: truemust only be used when persisting state globally across the application.- For typical in-component subscriptions, the default options should be used to allow automatic cleanup.
13.3.4. $reset Pattern
Unlike Options Stores, Setup Stores do not automatically provide a $reset() method. When state reset is needed, it must be implemented manually.
typescript
// src/stores/useFormStore.ts
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useFormStore = defineStore('form', () => {
const name = ref('')
const email = ref('')
const agreed = ref(false)
// Define a reset action to replace $reset.
function resetState() {
name.value = ''
email.value = ''
agreed.value = false
}
return { name, email, agreed, resetState }
})- Including a
resetState()action in every Setup Store is recommended. - When initial values are complex, a factory function should be used.
typescript
import { ref } from 'vue'
import { defineStore } from 'pinia'
import type { OrderDraft } from '@/types/order'
function createInitialState(): OrderDraft {
return {
items: [],
shippingAddress: null,
paymentMethod: null,
memo: '',
}
}
export const useOrderDraftStore = defineStore('order-draft', () => {
const draft = ref<OrderDraft>(createInitialState())
function resetState() {
draft.value = createInitialState()
}
return { draft, resetState }
})- The initial value factory function must be defined within the Store file and must return a new object on every invocation.