Skip to content

リアクティビティパターン

13.3.1. storeToRefs

Store から状態と getter を分割代入する際は必ず storeToRefs() を使用します。通常の分割代入を使用するとリアクティビティが失われます。

  • state、getter: storeToRefs() で分割代入します。
  • action: 直接分割代入します。action は通常の関数であるため storeToRefs() は不要です。
typescript
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/useUserStore'

const userStore = useUserStore()

// state、getter は storeToRefs で抽出します。
const { user, isLoggedIn, displayName } = storeToRefs(userStore)

// action は直接分割代入します。
const { loadCurrentUser, logout } = userStore

以下は正しくないパターンです。リアクティビティが失われ、テンプレートが更新されません。

typescript
// 禁止: リアクティビティが失われます。
const { user, isLoggedIn } = useUserStore()

13.3.2. computed の活用

Store 内部で派生状態は computed() で定義します。コンポーネントで同じ計算を繰り返しません。

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[]>([])

  // 派生状態は 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 }
})
  • 同じ派生ロジックを複数のコンポーネントで使用する場合、コンポーネントではなく Store の getter として定義します。
  • コンポーネントで Store の getter を利用する際は storeToRefs() を使用します。
typescript
// コンポーネントで getter を利用
const cartStore = useCartStore()
const { totalPrice, isEmpty } = storeToRefs(cartStore)

13.3.3. $subscribe

$subscribe() を使用して Store の状態変更を検知することができます。

typescript
const userStore = useUserStore()

userStore.$subscribe((mutation, state) => {
  // mutation.type: 'direct' | 'patch object' | 'patch function'
  // mutation.storeId: Store ID
  // state: 変更後の状態
  localStorage.setItem('user', JSON.stringify(state.user))
})

$subscribe() のオプションは以下の通りです。

オプション説明
detachedbooleantrue に設定するとコンポーネントのアンマウント後もサブスクリプションが維持されます。
deepbooleanネストされたオブジェクトの変更まで検知します。デフォルト値は true です。
flush'pre' | 'post' | 'sync'コールバックの実行タイミングを指定します。
typescript
// コンポーネントのアンマウント後もサブスクリプションを維持するパターン
userStore.$subscribe(
  (mutation, state) => {
    sessionStorage.setItem('user-state', JSON.stringify(state))
  },
  { detached: true }
)
  • detached: true はアプリ全体で状態を永続化する場合にのみ使用します。
  • 一般的なコンポーネント内のサブスクリプションはデフォルトオプションを使用して自動クリーンアップされるようにします。

13.3.4. $reset パターン

Setup Store は Options Store と異なり $reset() メソッドが自動提供されません。状態の初期化が必要な場合は直接実装しなければなりません。

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)

  // $reset を代替する初期化 action を定義します。
  function resetState() {
    name.value = ''
    email.value = ''
    agreed.value = false
  }

  return { name, email, agreed, resetState }
})
  • すべての Setup Store に resetState() action を含めることを推奨します。
  • 初期値が複雑な場合はファクトリ関数を活用します。
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 }
})
  • 初期値ファクトリ関数は Store ファイル内に定義し、呼び出しごとに新しいオブジェクトを返さなければなりません。

TIENIPIA QUALIFIED STANDARD