Pinia 狀態管理 + TypeScript
Pinia 是 Vue 3 官方推薦的狀態管理工具,原生支援 TypeScript。本篇將介紹完整的類型定義方式。
一、 基本設定
1.1 安裝
bash
npm install pinia1.2 初始化
typescript
// 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');二、 Option Store 類型
2.1 基本定義
typescript
// stores/counter.ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter',
}),
getters: {
double: (state) => state.count * 2,
formatted(): string {
return `${this.name}: ${this.count}`;
},
},
actions: {
increment() {
this.count++;
},
async fetchCount() {
const response = await fetch('/api/count');
this.count = await response.json();
},
},
});2.2 明確類型
typescript
import { defineStore } from 'pinia';
interface CounterState {
count: number;
name: string;
}
export const useCounterStore = defineStore('counter', {
state: (): CounterState => ({
count: 0,
name: 'Counter',
}),
getters: {
double(): number {
return this.count * 2;
},
},
actions: {
increment(): void {
this.count++;
},
},
});三、 Setup Store 類型(推薦)
3.1 基本定義
typescript
// stores/counter.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0);
const name = ref('Counter');
// getters
const double = computed(() => count.value * 2);
const formatted = computed(() => `${name.value}: ${count.value}`);
// actions
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
async function fetchCount() {
const response = await fetch('/api/count');
count.value = await response.json();
}
return {
count,
name,
double,
formatted,
increment,
decrement,
fetchCount,
};
});3.2 複雜類型
typescript
import { defineStore } from 'pinia';
import { ref, computed, type Ref } from 'vue';
interface User {
id: number;
name: string;
email: string;
}
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null);
const loading = ref(false);
const error = ref<Error | null>(null);
const isLoggedIn = computed(() => user.value !== null);
const userName = computed(() => user.value?.name ?? 'Guest');
async function login(email: string, password: string): Promise<boolean> {
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
user.value = await response.json();
return true;
} catch (e) {
error.value = e as Error;
return false;
} finally {
loading.value = false;
}
}
function logout() {
user.value = null;
}
return {
user,
loading,
error,
isLoggedIn,
userName,
login,
logout,
};
});四、 使用 Store
4.1 在元件中使用
vue
<script setup lang="ts">
import { useCounterStore } from "@/stores/counter";
const counterStore = useCounterStore();
// 直接存取
console.log(counterStore.count);
console.log(counterStore.double);
// 呼叫 action
counterStore.increment();
</script>
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<p>Double: {{ counterStore.double }}</p>
<button @click="counterStore.increment">+1</button>
</div>
</template>4.2 解構並保持響應性
vue
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useCounterStore } from "@/stores/counter";
const counterStore = useCounterStore();
// 使用 storeToRefs 解構 state 和 getters
const { count, double } = storeToRefs(counterStore);
// actions 可以直接解構
const { increment, decrement } = counterStore;
</script>五、 Store 之間的互動
5.1 在 Store 中使用其他 Store
typescript
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([]);
const userStore = useUserStore();
const total = computed(() => {
const subtotal = items.value.reduce((sum, item) => sum + item.price, 0);
// 根據使用者等級計算折扣
const discount = userStore.isVip ? 0.1 : 0;
return subtotal * (1 - discount);
});
return { items, total };
});六、 Plugin 類型
6.1 定義 Plugin
typescript
import { type PiniaPluginContext } from 'pinia';
function myPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation, state) => {
console.log(`[${mutation.storeId}] ${mutation.type}`);
});
// 添加屬性
store.$state.pluginData = 'added by plugin';
}
// 使用
const pinia = createPinia();
pinia.use(myPlugin);6.2 擴展 Store 類型
typescript
import 'pinia';
declare module 'pinia' {
export interface PiniaCustomProperties {
customProperty: string;
customMethod(): void;
}
export interface PiniaCustomStateProperties<S> {
pluginData: string;
}
}七、 持久化
7.1 使用 pinia-plugin-persistedstate
bash
npm install pinia-plugin-persistedstatetypescript
// main.ts
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);typescript
// stores/user.ts
export const useUserStore = defineStore(
'user',
() => {
const token = ref<string | null>(null);
const user = ref<User | null>(null);
return { token, user };
},
{
persist: true,
}
);7.2 自訂持久化
typescript
export const useUserStore = defineStore(
'user',
() => {
// ...
},
{
persist: {
key: 'user-store',
storage: localStorage,
pick: ['token'], // 只持久化 token
},
}
);八、 實用範例
8.1 購物車 Store
typescript
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
interface Product {
id: number;
name: string;
price: number;
}
interface CartItem extends Product {
quantity: number;
}
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([]);
const count = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
);
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
function addItem(product: Product) {
const existing = items.value.find((item) => item.id === product.id);
if (existing) {
existing.quantity++;
} else {
items.value.push({ ...product, quantity: 1 });
}
}
function removeItem(productId: number) {
const index = items.value.findIndex((item) => item.id === productId);
if (index > -1) {
items.value.splice(index, 1);
}
}
function updateQuantity(productId: number, quantity: number) {
const item = items.value.find((item) => item.id === productId);
if (item) {
item.quantity = Math.max(0, quantity);
if (item.quantity === 0) {
removeItem(productId);
}
}
}
function clear() {
items.value = [];
}
return {
items,
count,
total,
addItem,
removeItem,
updateQuantity,
clear,
};
});總結
| 方式 | 特點 | 推薦 |
|---|---|---|
| Option Store | 類似 Options API | 熟悉 Vuex |
| Setup Store | 類似 Composition API | 推薦使用 |
| API | 用途 |
|---|---|
defineStore() | 定義 Store |
storeToRefs() | 解構並保持響應性 |
store.$subscribe() | 監聽狀態變化 |
store.$reset() | 重置狀態 |
> **推薦做法**:
- 使用 Setup Store 語法
- 複雜類型使用 interface
- 善用 storeToRefs 解構
進階挑戰
- 實作一個 TODO Store,支援增刪改查
- 建立一個身份驗證 Store,處理 JWT Token