Chuyển đến nội dung chính

レッスン 6: Pinia による状態管理

ピニア対ビュークス。ストア、状態、ゲッター、アクションを定義します。ストア構成、プラグイン。永続的な状態、devtools の統合。ストア パターン、モジュール型ストア。

💻 プログラミング — レッスン 6 レッスン 6: Pinia による状態管理

Vue.js と Nuxt: 基本から高度まで

パート 2: Vue エコシステム

xdev.asia

1.ピニアのセットアップ

// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

2. 店舗の定義

// stores/useAuthStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// Setup syntax (recommended)
export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null)
  const token = ref<string | null>(null)
  const isLoading = ref(false)

  // Getters
  const isAuthenticated = computed(() => !!token.value)
  const isAdmin = computed(() => user.value?.role === 'admin')

  // Actions
  async function login(email: string, password: string) {
    isLoading.value = true
    try {
      const res = await fetch('/api/auth/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
      })
      const data = await res.json()
      user.value = data.user
      token.value = data.token
    } finally {
      isLoading.value = false
    }
  }

  function logout() {
    user.value = null
    token.value = null
  }

  return { user, token, isLoading, isAuthenticated, isAdmin, login, logout }
})

3.ストアを利用する

<script setup lang="ts">
import { useAuthStore } from '@/stores/useAuthStore'
import { storeToRefs } from 'pinia'

const authStore = useAuthStore()

// ⚠️ Cần storeToRefs để giữ reactivity khi destructure
const { user, isAuthenticated, isLoading } = storeToRefs(authStore)

// Actions có thể destructure trực tiếp
const { login, logout } = authStore
</script>

<template>
  <div v-if="isAuthenticated">
    <p>Xin chào, {{ user?.name }}</p>
    <button @click="logout">Đăng xuất</button>
  </div>
  <div v-else>
    <button @click="login('[email protected]', '123')" :disabled="isLoading">
      {{ isLoading ? 'Đang đăng nhập...' : 'Đăng nhập' }}
    </button>
  </div>
</template>

4. 店舗構成

// stores/useCartStore.ts
export const useCartStore = defineStore('cart', () => {
  const authStore = useAuthStore() // Sử dụng store khác

  const items = ref<CartItem[]>([])

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
  )

  async function checkout() {
    if (!authStore.isAuthenticated) {
      throw new Error('Please login first')
    }
    await fetch('/api/checkout', {
      method: 'POST',
      headers: { Authorization: `Bearer ${authStore.token}` },
      body: JSON.stringify({ items: items.value }),
    })
    items.value = []
  }

  return { items, total, checkout }
})

5.ピニアプラグイン

// plugins/piniaPersistedState.ts
import { watch } from 'vue'
import type { PiniaPluginContext } from 'pinia'

export function persistedState({ store }: PiniaPluginContext) {
  const key = `pinia-${store.$id}`

  // Restore state from localStorage
  const saved = localStorage.getItem(key)
  if (saved) {
    store.$patch(JSON.parse(saved))
  }

  // Persist on change
  watch(
    () => store.$state,
    (state) => {
      localStorage.setItem(key, JSON.stringify(state))
    },
    { deep: true }
  )
}

// main.ts
const pinia = createPinia()
pinia.use(persistedState)

6. オプション構文 (代替)

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

次の記事: 構成可能で再利用可能なロジック — カスタム コンポーザブル、VueUse。