Skip to content

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 } = userStore

The 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.

OptionTypeDescription
detachedbooleanWhen set to true, the subscription persists after the component is unmounted.
deepbooleanDetects 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: true must 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.

TIENIPIA QUALIFIED STANDARD