Vue — Pinia
テクノロジ系 / L05 state-management — 状態管理
Vue — Pinia
Pinia とは
Vue 3 公式推奨の 状態管理ライブラリ:
- Vuex の後継
- TypeScript 親和性高
- ボイラープレート少ない
- DevTools サポート
- Composition API ネイティブ
$ pnpm add pinia
setup
main.ts:
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
createApp(App).use(createPinia()).mount("#app");store 定義 (Setup Store スタイル・推奨)
stores/counter.ts:
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useCounterStore = defineStore("counter", () => {
// state
const count = ref(0);
// getter
const doubled = computed(() => count.value * 2);
// action
function increment() {
count.value++;
}
async function fetchInitial() {
const res = await fetch("/api/count");
count.value = (await res.json()).count;
}
return { count, doubled, increment, fetchInitial };
});<script setup> と同じ感覚で書ける。
使う
<script setup>
import { useCounterStore } from "@/stores/counter.js";
import { storeToRefs } from "pinia";
const counter = useCounterStore();
const { count, doubled } = storeToRefs(counter); // reactivity を保つ
const { increment } = counter; // actions は分割可
</script>
<template>
<p>{{ count }} ({{ doubled }})</p>
<button @click="increment">+</button>
</template>storeToRefs を 忘れない。
const { count } = counter だとリアクティブが切れる。
Options Store スタイル
export const useCounterStore = defineStore("counter", {
state: () => ({ count: 0 }),
getters: {
doubled: (state) => state.count * 2,
},
actions: {
increment() {
this.count++;
},
},
});Vuex に近い書き方。
新規は Setup Store が公式推奨。
TypeScript 推論
const counter = useCounterStore(); counter.count; // number counter.doubled; // number counter.increment(); // ✅ counter.unknown; // ❌ TS error
→ defineStore から完全に推論。
$reset / $patch
counter.$reset(); // 初期状態に戻す
counter.$patch({ count: 100 }); // 複数キーを一括更新
counter.$patch((state) => { // 関数で柔軟に
state.count++;
state.items.push("new");
});DevTools
Vue DevTools の Pinia タブで:
- 各 store の state 確認
- 時系列 (タイムトラベル)
- action 履歴
→ React の Redux DevTools と同等の体験。
まとめ
- Pinia は Vue 3 公式の状態管理 (Vuex 後継)
defineStore("name", () => { ref/computed/function })で定義useXxxStore()で利用、storeToRefsでリアクティブ保持- DevTools が強力
- TS 推論が完璧
次は Composables。
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
Pinia — 演習
pnpm add pinia
Lv1: 基本 — Counter store
問題
// stores/counter.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() { count.value++; }
function reset() { count.value = 0; }
return { count, doubled, increment, reset };
});// main.ts
import { createPinia } from "pinia";
createApp(App).use(createPinia()).mount("#app");<!-- どこかの component -->
<script setup>
import { useCounterStore } from "@/stores/counter.js";
import { storeToRefs } from "pinia";
const counter = useCounterStore();
const { count, doubled } = storeToRefs(counter);
const { increment, reset } = counter;
</script>
<template>
<p>{{ count }} ({{ doubled }})</p>
<button @click="increment">+</button>
<button @click="reset">0</button>
</template>合格基準
- store が動く
- storeToRefs を忘れない
Lv2: 応用 — 非同期 action
問題
export const useUserStore = defineStore("user", () => {
const user = ref<any>(null);
const loading = ref(false);
async function fetchUser(id: number) {
loading.value = true;
try {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
user.value = await res.json();
} finally {
loading.value = false;
}
}
return { user, loading, fetchUser };
});<script setup>
const userStore = useUserStore();
const { user, loading } = storeToRefs(userStore);
const { fetchUser } = userStore;
fetchUser(1);
</script>合格基準
- 非同期 action が動く
- loading state の遷移を観察
Lv3: 発展 — persist 風 (manual)
問題
import { defineStore } from "pinia";
import { ref, watch } from "vue";
export const useThemeStore = defineStore("theme", () => {
const theme = ref<"light" | "dark">(
(localStorage.getItem("theme") as "light" | "dark") ?? "light"
);
watch(theme, (v) => localStorage.setItem("theme", v));
function toggle() {
theme.value = theme.value === "light" ? "dark" : "light";
}
return { theme, toggle };
});リロード後も theme が保持されることを確認。
合格基準
- localStorage と同期
- リロードで復元
提出
submission.md に3問の回答を。